Virtual Environments
Reviewed & published by Brayan K
A virtual environment is an isolated, per-project Python installation that keeps each project's packages and versions separate, so dependencies never clash and your installs stay reproducible.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Master professional Python dependency management, virtual environments, and reproducible installs.
🧰 What You'll Learn
This lesson teaches how to build professional Python applications with:
- Virtual environments for project isolation
- Clean dependency management
- Reproducible installs across machines
- Modern tooling (pip-tools, Poetry, UV)
- Security and supply-chain safety
- CI/CD integration
This separates "it works on my laptop" scripts from real, deployable software.
📥 Python Download & Setup
Download Python from: python.org/downloads
Latest version recommended (3.11+)
Part 1: Virtual Environment Fundamentals
🔥 1. Why You Should Never Rely on "System Python"
On most machines, you'll have System Python (used by OS tools) and Your project Python (what you control).
If you install packages globally with pip install requests, you risk:
| ❌ Problem | What Happens | ✅ Virtual Env Solution |
|---|---|---|
| Version conflict | Project A needs Django 3.2, Project B needs Django 5.0 | Each project has its own Django version |
| OS breaks | You update a package the OS depends on | System Python stays untouched |
| Dependency chaos | Can't remember which packages belong to which project | Each project has its own requirements.txt |
Rule #1: Each serious project gets its own isolated environment.
🧪 2. What a Virtual Environment Actually Is
A virtual environment is:
- A folder containing its own Python interpreter
- With its own site-packages directory
- Isolated from global packages
Two projects can now safely require:
- Project A → Django==3.2
- Project B → Django==5.0
No conflict, because they use different environments.
⚙️ 3. Creating a Virtual Environment (venv)
Standard library way (no extra installs):
# Use the Python you want (3.10 example)
# Run in terminal: python3.10 -m venv .venv
# This creates a .venv/ folder in your project
print("Virtual environment commands:")
print("python3.10 -m venv .venv")
print("This creates a .venv/ folder in your project")
# ✅ Expected output:
# Virtual environment commands:
# python3.10 -m venv .venv
# This creates a .venv/ folder in your projectActivating it:
# Run in terminal:
# source .venv/bin/activate
print("Activation command for macOS/Linux/WSL:")
print("source .venv/bin/activate")
# ✅ Expected output:
# Activation command for macOS/Linux/WSL:
# source .venv/bin/activate# Run in PowerShell:
# .venv\Scripts\Activate.ps1
print("Activation command for Windows PowerShell:")
print(".venv\\Scripts\\Activate.ps1")
# ✅ Expected output:
# Activation command for Windows PowerShell:
# .venv\Scripts\Activate.ps1Your prompt usually changes to:
- python = the one inside .venv
- pip = the one inside .venv
Deactivating:
# Run in terminal:
# deactivate
print("Deactivation command:")
print("deactivate")
# ✅ Expected output:
# Deactivation command:
# deactivate🧱 4. Best Practices for Environment Layout
Recommended pattern per project:
# Recommended project structure:
project_structure = '''
my_project/
├─ .venv/ # virtual environment (not committed)
├─ src/ or app/ # your code
├─ tests/
├─ requirements.txt # pinned dependencies
├─ requirements-dev.txt
├─ pyproject.toml # (optional, for modern tooling)
└─ README.md
'''
print(project_structure)
# ✅ Expected output:
#
# my_project/
# ├─ .venv/ # virtual environment (not committed)
# ├─ src/ or app/ # your code
# ├─ tests/
# ├─ requirements.txt # pinned dependencies
# ├─ requirements-dev.txt
# ├─ pyproject.toml # (optional, for modern tooling)
# └─ README.md- Consistent across all your projects
- Editors like VS Code automatically detect it
- Easy to add to .gitignore
# Add these to .gitignore:
gitignore_entries = '''
.venv/
__pycache__/
*.pyc
'''
print(".gitignore entries:")
print(gitignore_entries)
# ✅ Expected output:
# .gitignore entries:
#
# .venv/
# __pycache__/
# *.pycNever commit your virtual environment to Git.
💊 5. Installing Packages the Right Way
Once your environment is activated:
# Run in terminal (with venv activated):
# python -m pip install requests fastapi uvicorn
print("Install command:")
print("python -m pip install requests fastapi uvicorn")
# ✅ Expected output:
# Install command:
# python -m pip install requests fastapi uvicornWhy python -m pip?
- Guarantees you're using the pip tied to that Python
- Avoids weird "wrong pip" issues
# Run in terminal:
# python -m pip list
print("List installed packages:")
print("python -m pip list")
# ✅ Expected output:
# List installed packages:
# python -m pip listAvoid installing directly with pip outside an activated environment.
📦 6. requirements.txt & Pinning Versions
For reproducibility, you want to freeze exactly which versions you're using.
# Run in terminal:
# python -m pip freeze > requirements.txt
print("Freeze command:")
print("python -m pip freeze > requirements.txt")
# ✅ Expected output:
# Freeze command:
# python -m pip freeze > requirements.txtThis file might look like:
# Example requirements.txt content:
requirements = '''
fastapi==0.115.0
uvicorn==0.32.0
pydantic==2.9.0
requests==2.32.3
'''
print("requirements.txt:")
print(requirements)
# ✅ Expected output:
# requirements.txt:
#
# fastapi==0.115.0
# uvicorn==0.32.0
# pydantic==2.9.0
# requests==2.32.3Now anyone can recreate your environment:
# Commands to recreate environment:
commands = '''
python -m venv .venv
source .venv/bin/activate # or Windows equivalent
python -m pip install -r requirements.txt
'''
print("Recreate environment:")
print(commands)
# ✅ Expected output:
# Recreate environment:
#
# python -m venv .venv
# source .venv/bin/activate # or Windows equivalent
# python -m pip install -r requirements.txtThis is critical for:
- Deploying to servers
- Running on a teammate's machine
- Long-term maintenance
🔍 7. Semantic Versioning & Safer Constraints
Not all dependencies need to be fully pinned, but you should understand version ranges:
| Syntax | Meaning | When to Use |
|---|---|---|
| ==1.4.3 | Exact version only | Production apps (safest) |
| >=1.4,<2.0 | Any 1.x version | Libraries you publish |
| ~=1.4 | Compatible release: 1.4 or newer, but still 1.x (same as >=1.4,<2.0) | Balanced approach |
| ~=1.4.0 | Compatible release: 1.4.0 or newer, but still 1.4.x | Patch updates only |
For libraries you publish: allow ranges (e.g. >=1.4,<2.0)
For apps you deploy: pinned versions (==) are safest
🧑🏫 Worked example: audit a requirements file
Reading version specifiers is something you'll do every week, so here is a complete program that does it for you. It walks a requirements file line by line, works out what each constraint actually allows, and tells you which packages could change version behind your back. Read the comments first, then run it.
# WORKED EXAMPLE: a tiny "requirements auditor".
# It reads a requirements file and tells you which lines are safely pinned
# (locked to one exact version) and which could change under your feet.
# On your own machine you would read the real file:
# text = open("requirements.txt").read()
# Here the contents sit in a string so this runs anywhere.
requirements_txt = """
# core runtime
fastapi==0.115.0
uvicorn==0.32.0
requests>=2.32,<3.0
pydantic~=2.9.0
python-dotenv
"""
def split_line(line):
"""Split 'fastapi==0.115.0' into ('fastapi', '==0.115.0')."""
for i, char in enumerate(line): # walk the text one character at a time
if char in "=<>~!": # the first specifier character starts the constraint
return line[:i].strip(), line[i:].strip()
return line.strip(), "" # no specifier at all -> empty constraint
def classify(constraint):
"""Turn a constraint into a plain-English risk label."""
if constraint.startswith("=="):
return "PINNED", "exact version - reproducible"
if constraint == "":
return "FLOATING", "any version at all - risky in production"
return "RANGE", "may install a newer release later"
print("=== requirements.txt audit ===")
unpinned = 0 # counter for the summary line
for raw in requirements_txt.splitlines(): # splitlines() gives one string per line
line = raw.strip()
if not line or line.startswith("#"): # skip blank lines and comments
continue
name, constraint = split_line(line)
label, why = classify(constraint)
if label != "PINNED":
unpinned += 1
shown = constraint if constraint else "(none)"
# ljust() pads with spaces so the columns line up
print(f"{name.ljust(14)} {shown.ljust(13)} {label.ljust(9)} {why}")
print()
print(f"{unpinned} of these lines are not pinned.")
print("For an app you deploy, pin every one of them with ==.")
# ✅ Output:
# === requirements.txt audit ===
# fastapi ==0.115.0 PINNED exact version - reproducible
# uvicorn ==0.32.0 PINNED exact version - reproducible
# requests >=2.32,<3.0 RANGE may install a newer release later
# pydantic ~=2.9.0 RANGE may install a newer release later
# python-dotenv (none) FLOATING any version at all - risky in production
#
# 3 of these lines are not pinned.
# For an app you deploy, pin every one of them with ==.
# ✅ Expected output:
# === requirements.txt audit ===
# fastapi ==0.115.0 PINNED exact version - reproducible
# uvicorn ==0.32.0 PINNED exact version - reproducible
# requests >=2.32,<3.0 RANGE may install a newer release later
# pydantic ~=2.9.0 RANGE may install a newer release later
# python-dotenv (none) FLOATING any version at all - risky in production
#
# 3 of these lines are not pinned.
# For an app you deploy, pin every one of them with ==.🧪 8. Separating Runtime vs Dev Dependencies
Don't ship your test tools to production. Use two files:
# requirements.txt (runtime only):
runtime_deps = '''
fastapi==0.115.0
uvicorn==0.32.0
pydantic==2.9.0
'''
print("requirements.txt (runtime):")
print(runtime_deps)
# ✅ Expected output:
# requirements.txt (runtime):
#
# fastapi==0.115.0
# uvicorn==0.32.0
# pydantic==2.9.0# requirements-dev.txt (development):
dev_deps = '''
-r requirements.txt
pytest==8.3.0
black==24.10.0
ruff==0.7.0
mypy==1.13.0
'''
print("requirements-dev.txt:")
print(dev_deps)
# ✅ Expected output:
# requirements-dev.txt:
#
# -r requirements.txt
# pytest==8.3.0
# black==24.10.0
# ruff==0.7.0
# mypy==1.13.0Then install dev tools like:
# Install dev dependencies:
print("python -m pip install -r requirements-dev.txt")
# ✅ Expected output:
# python -m pip install -r requirements-dev.txtYour production environment only installs requirements.txt, keeping it:
- Faster to deploy
🎯 Your turn: build the two files
Now do the split yourself. Everything below is written for you except three blanks — the pip include prefix, the exact-pin specifier, and the check that counts pinned lines. Replace each ___ and run it.
# 🎯 YOUR TURN - split one package list into a runtime file and a dev file
# Replace each ___ below, then run.
runtime = ["fastapi==0.115.0", "uvicorn==0.32.0", "pydantic==2.9.0"]
dev_only = ["pytest==8.3.0", "black==24.10.0", "ruff==0.7.0"]
# 1) requirements-dev.txt starts by pulling in the runtime file.
INCLUDE = ___ # 👉 replace ___ with the pip include prefix, in quotes: "-r "
# 2) The specifier that locks a package to one exact version.
PIN = ___ # 👉 replace ___ with that two-character specifier, in quotes
print("--- requirements.txt (this is all production installs) ---")
for dep in runtime:
print(dep)
print()
print("--- requirements-dev.txt (your laptop and CI) ---")
print(INCLUDE + "requirements.txt")
for dep in dev_only:
print(dep)
print()
# 3) Count only the lines locked to an exact version.
pinned = [d for d in runtime + dev_only if ___ in d] # 👉 replace ___ with the name you used in step 2
print(f"Exactly pinned: {len(pinned)} of {len(runtime) + len(dev_only)} lines")
print(f"Production ships {len(runtime)} packages, not {len(runtime) + len(dev_only)}.")
# ✅ Expected output once the blanks are filled in:
# --- requirements.txt (this is all production installs) ---
# fastapi==0.115.0
# uvicorn==0.32.0
# pydantic==2.9.0
#
# --- requirements-dev.txt (your laptop and CI) ---
# -r requirements.txt
# pytest==8.3.0
# black==24.10.0
# ruff==0.7.0
#
# Exactly pinned: 6 of 6 lines
# Production ships 3 packages, not 6.🧰 9. Using pip-tools / Poetry / UV (Modern Workflows)
As projects get bigger, plain pip freeze becomes messy. Three common modern approaches:
1) pip-tools
# pip-tools workflow:
workflow = '''
pip install pip-tools
# requirements.in:
# fastapi
# uvicorn
# Compile:
pip-compile requirements.in
# Install:
pip-sync requirements.txt
'''
print("pip-tools workflow:")
print(workflow)
# ✅ Expected output:
# pip-tools workflow:
#
# pip install pip-tools
#
# # requirements.in:
# # fastapi
# # uvicorn
#
# # Compile:
# pip-compile requirements.in
#
# # Install:
# pip-sync requirements.txtThis ensures your environment matches exactly the file.
2) Poetry
Uses pyproject.toml + poetry.lock. You define high-level deps; Poetry resolves & locks everything.
3) UV / Rye / Hatch
Newer tools that combine:
- Env creation
- Dependency resolution
🌍 10. Global Python Tools: Use pipx, Not Your Project venv
Some Python packages are tools, not libraries, e.g.:
These are better installed with pipx globally:
# Install global tools with pipx:
print("pip install pipx")
print("pipx install httpie")
# ✅ Expected output:
# pip install pipx
# pipx install httpie- Keeps them isolated from projects
- Avoids interfering with your app dependencies
- Lets you use tools from your shell, no activation needed
🧠 11. Environment Rebuild Strategy
A good habit whenever things feel "weird":
# Environment rebuild steps:
steps = '''
# 1. Delete .venv/
rm -rf .venv
# 2. Re-create environment
python -m venv .venv
source .venv/bin/activate
# 3. Reinstall from requirements.txt
python -m pip install -r requirements.txt
'''
print("Rebuild steps:")
print(steps)
# ✅ Expected output:
# Rebuild steps:
#
# # 1. Delete .venv/
# rm -rf .venv
#
# # 2. Re-create environment
# python -m venv .venv
# source .venv/bin/activate
#
# # 3. Reinstall from requirements.txt
# python -m pip install -r requirements.txtIf the problem disappears, it was likely:
- Stale dependencies
- Partial upgrades
- Broken wheels
This rebuild pattern is common in real teams.
Part 2: Advanced Dependency Management
🧩 12. Solving Dependency Conflicts Like a Pro
You'll eventually hit something like:
This is a dependency conflict — two packages require incompatible versions of a dependency.
How to diagnose the conflict:
Install a dependency tree visualizer:
# Diagnose dependency conflicts:
commands = '''
pip install pipdeptree
pipdeptree
# Example output:
# fastapi==0.115.0
# - pydantic==2.9.0
# old-tool==1.3.0
# - pydantic==1.10.0
'''
print("Dependency tree commands:")
print(commands)
# ✅ Expected output:
# Dependency tree commands:
#
# pip install pipdeptree
# pipdeptree
#
# # Example output:
# # fastapi==0.115.0
# # - pydantic==2.9.0
# # old-tool==1.3.0
# # - pydantic==1.10.0How to fix it:
- Identify which package is outdated
- Upgrade/downgrade the conflicting package
- If successful: pip freeze > requirements.txt
🧱 13. Lockfiles & Reproducible Installs
# requirements.txt acts as a lockfile:
# - pin all versions
# - ensure identical installs for every machine
print("Upgrade and freeze:")
print("pip install --upgrade fastapi")
print("pip freeze > requirements.txt")
# ✅ Expected output:
# Upgrade and freeze:
# pip install --upgrade fastapi
# pip freeze > requirements.txtWith pip-tools (recommended):
Keep human-friendly requirements in requirements.in:
# requirements.in (human-friendly):
requirements_in = '''
fastapi
uvicorn
'''
print("requirements.in:")
print(requirements_in)
# ✅ Expected output:
# requirements.in:
#
# fastapi
# uvicornCompile into a fully pinned lockfile:
# Compile to lockfile:
print("pip-compile requirements.in")
# ✅ Expected output:
# pip-compile requirements.inIt produces requirements.txt with all sub-dependencies pinned.
Keep one simple file for what you WANT, another for the exact resolved versions you MUST use.
🧬 14. Per-Environment Dependencies (Dev, Test, Production)
Large apps usually have:
- dev environment (testing, debugging tools)
- production environment (runtime packages only)
- testing environment (for CI)
A clean structure looks like:
# Multi-environment requirements structure:
structure = '''
requirements/
├─ base.in
├─ dev.in
├─ prod.in
├─ base.txt
├─ dev.txt
└─ prod.txt
'''
print(structure)
# ✅ Expected output:
#
# requirements/
# ├─ base.in
# ├─ dev.in
# ├─ prod.in
# ├─ base.txt
# ├─ dev.txt
# └─ prod.txtExample:
# requirements/base.in:
base = '''
fastapi
sqlalchemy
pydantic
'''
print("base.in:")
print(base)
# ✅ Expected output:
# base.in:
#
# fastapi
# sqlalchemy
# pydantic# requirements/dev.in:
dev = '''
-r base.in
pytest
mypy
black
'''
print("dev.in:")
print(dev)
# ✅ Expected output:
# dev.in:
#
# -r base.in
# pytest
# mypy
# black# requirements/prod.in:
prod = '''
-r base.in
gunicorn
prometheus-client
'''
print("prod.in:")
print(prod)
# ✅ Expected output:
# prod.in:
#
# -r base.in
# gunicorn
# prometheus-client# Compile all requirement files:
print("pip-compile requirements/base.in")
print("pip-compile requirements/dev.in")
print("pip-compile requirements/prod.in")
# ✅ Expected output:
# pip-compile requirements/base.in
# pip-compile requirements/dev.in
# pip-compile requirements/prod.in# Install for specific environment:
print("pip install -r requirements/dev.txt")
print("pip install -r requirements/prod.txt")
# ✅ Expected output:
# pip install -r requirements/dev.txt
# pip install -r requirements/prod.txt🔐 15. Internal Libraries & Private Packages
Businesses often create reusable internal libraries. These can be installed:
1. As editable local packages:
# Install local package in editable mode:
print("pip install -e ./core_engine")
# ✅ Expected output:
# pip install -e ./core_engine2. From a private PyPI server:
# Install from private PyPI:
print("pip install core-engine --extra-index-url https://packages.acmetools.com/simple")
# ✅ Expected output:
# pip install core-engine --extra-index-url https://packages.acmetools.com/simpleBest practices:
- Use semantic versioning (1.0.0, 1.1.0, etc.)
- Pin specific versions in your main project
- Avoid installing directly from main branches
🛡 16. Security & Supply-Chain Safety
Dependencies are attack vectors. Follow these rules:
✔ 1. Scan dependencies regularly:
# Scan for vulnerabilities:
print("pip install pip-audit")
print("pip-audit")
# ✅ Expected output:
# pip install pip-audit
# pip-audit✔ 2. Stick with reputable packages
Prefer libraries that have:
- Lots of downloads
- Active maintenance
- Frequent updates
- Reliable documentation
✔ 3. Pin versions in production
Floating version ranges can pull in a bad update.
✔ 4. Audit new dependencies
- Do I really need this?
- Is this package trustworthy?
- Could I write this functionality myself?
Small dependency trees = safer, easier to maintain.
🐳 17. Using Virtual Environments Inside Docker
Docker isolates processes, but venvs add clarity and consistency.
# Example Dockerfile with venv:
dockerfile = '''
FROM python:3.11-slim
WORKDIR /app
RUN python -m venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]
'''
print(dockerfile)
# ✅ Expected output:
#
# FROM python:3.11-slim
#
# WORKDIR /app
#
# RUN python -m venv /app/.venv
# ENV PATH="/app/.venv/bin:$PATH"
#
# COPY requirements.txt .
# RUN pip install --no-cache-dir -r requirements.txt
#
# COPY . .
#
# CMD ["python", "main.py"]Local + Docker both use .venv → consistent setup.
⚙️ 18. Clean Install Testing in CI Pipelines
In CI tools like GitHub Actions or GitLab:
- Create fresh venv
- Install dependencies
# GitHub Actions workflow example:
workflow = '''
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Create venv
run: python -m venv .venv
- name: Install deps
run: |
source .venv/bin/activate
pip install -r requirements-dev.txt
- name: Run tests
run: |
source .venv/bin/activate
pytest
'''
print(workflow)
# ✅ Expected output:
#
# - uses: actions/setup-python@v5
# with:
# python-version: "3.11"
#
# - name: Create venv
# run: python -m venv .venv
#
# - name: Install deps
# run: |
# source .venv/bin/activate
# pip install -r requirements-dev.txt
#
# - name: Run tests
# run: |
# source .venv/bin/activate
# pytestThis ensures your project:
- Installs cleanly
- Has no hidden local dependencies
- Is production-ready
🧠 19. Workflow & Naming Conventions
Clean conventions prevent chaos:
- Always name virtual env .venv
- Use requirements.in + requirements.txt OR Poetry (don't mix)
- Document setup in README.md
- Never use global interpreter for real apps
- Recreate environment after major upgrades
Example recommended README snippet:
# README setup instructions:
readme = '''
1. python -m venv .venv
2. source .venv/bin/activate
3. pip install -r requirements-dev.txt
'''
print(readme)
# ✅ Expected output:
#
# 1. python -m venv .venv
# 2. source .venv/bin/activate
# 3. pip install -r requirements-dev.txt🎯 20. The Master Mental Model
Everything you do around dependencies fits into this model:
Python version → Virtual Environment → Dependencies → Lockfile
- Change Python version → everything can break
- Skip virtual env → dependencies collide
- Forget to freeze versions → inconsistent behavior
- Skip lockfile → unpredictable installs
Master this, and your projects become:
- ✔ Reproducible
- ✔ Easy to debug
- ✔ Easy to deploy
Part 3: Professional-Grade Patterns & Production
🧪 21. Example: Clean Setup Workflow for a New Project
A solid, repeatable pattern for any new Python project:
# Complete new project setup:
setup = '''
# 1. Create the project folder
mkdir awesome-project
cd awesome-project
# 2. Create and activate a virtual environment
python -m venv .venv
# macOS / Linux
source .venv/bin/activate
# Windows (PowerShell)
.venv\\Scripts\\Activate.ps1
# 3. Install core dependencies
pip install fastapi uvicorn pydantic
# 4. Freeze versions
pip freeze > requirements.txt
# 5. Install dev-only tools
pip install pytest black mypy
pip freeze > requirements-dev.txt
'''
print(setup)
# ✅ Expected output:
#
# # 1. Create the project folder
# mkdir awesome-project
# cd awesome-project
#
# # 2. Create and activate a virtual environment
# python -m venv .venv
#
# # macOS / Linux
# source .venv/bin/activate
#
# # Windows (PowerShell)
# .venv\Scripts\Activate.ps1
#
# # 3. Install core dependencies
# pip install fastapi uvicorn pydantic
#
# # 4. Freeze versions
# pip freeze > requirements.txt
#
# # 5. Install dev-only tools
# pip install pytest black mypy
# pip freeze > requirements-dev.txtExample structure:
# Project structure:
structure = '''
awesome-project/
├─ .venv/
├─ app/
│ ├─ __init__.py
│ └─ main.py
├─ tests/
├─ requirements.txt
├─ requirements-dev.txt
└─ README.md
'''
print(structure)
# ✅ Expected output:
#
# awesome-project/
# ├─ .venv/
# ├─ app/
# │ ├─ __init__.py
# │ └─ main.py
# ├─ tests/
# ├─ requirements.txt
# ├─ requirements-dev.txt
# └─ README.mdAnyone who clones this can run:
# After cloning the project:
print("python -m venv .venv")
print("source .venv/bin/activate")
print("pip install -r requirements-dev.txt")
# ✅ Expected output:
# python -m venv .venv
# source .venv/bin/activate
# pip install -r requirements-dev.txtand be ready to work.
🏗 22. Example: Multi-Service Environment Structure
For a slightly bigger setup (e.g. API + worker + shared library):
# Multi-service project structure:
structure = '''
project-root/
├─ services/
│ ├─ api/
│ │ ├─ app/
│ │ └─ requirements.txt
│ ├─ worker/
│ │ ├─ worker_app/
│ │ └─ requirements.txt
├─ libs/
│ └─ shared_lib/
│ └─ setup.py or pyproject.toml
└─ README.md
'''
print(structure)
# ✅ Expected output:
#
# project-root/
# ├─ services/
# │ ├─ api/
# │ │ ├─ app/
# │ │ └─ requirements.txt
# │ ├─ worker/
# │ │ ├─ worker_app/
# │ │ └─ requirements.txt
# ├─ libs/
# │ └─ shared_lib/
# │ └─ setup.py or pyproject.toml
# └─ README.mdEach service can have its own virtual environment:
# Create venv for a specific service:
print("cd services/api")
print("python -m venv .venv")
print("source .venv/bin/activate")
print("pip install -r requirements.txt")
# ✅ Expected output:
# cd services/api
# python -m venv .venv
# source .venv/bin/activate
# pip install -r requirements.txtShared library can be installed in editable mode:
# Install shared library:
print("cd project-root")
print("pip install -e libs/shared_lib")
# ✅ Expected output:
# cd project-root
# pip install -e libs/shared_libThis keeps boundaries clear, especially when different services need different versions of frameworks.
🧩 24. Handling OS-Specific and Optional Dependencies
Some packages are only needed on certain platforms or for extra features.
Optional dependencies pattern:
# Optional dependencies pattern:
pattern = '''
# Base requirements:
# - fastapi
# - uvicorn
# Dev extras:
# - pytest
# - black
# Optional "redis" extras:
# - redis
'''
print(pattern)
# ✅ Expected output:
#
# # Base requirements:
# # - fastapi
# # - uvicorn
#
# # Dev extras:
# # - pytest
# # - black
#
# # Optional "redis" extras:
# # - redisKeep separate requirements-*.txt files:
# Separate requirements files:
files = '''
requirements.txt # core
requirements-dev.txt # dev tools
requirements-redis.txt # extra feature
'''
print(files)
# ✅ Expected output:
#
# requirements.txt # core
# requirements-dev.txt # dev tools
# requirements-redis.txt # extra featureInstall only what's needed:
# Install multiple requirements:
print("pip install -r requirements.txt -r requirements-redis.txt")
# ✅ Expected output:
# pip install -r requirements.txt -r requirements-redis.txtProblem 1: "It works on my machine but not on theirs"
- No pinned versions (requirements.txt missing or incomplete)
- Different Python version
- Some packages installed globally by accident
- Ensure everyone uses the same Python major/minor (e.g. 3.11)
- Use a venv and requirements*.txt files
- Regenerate lockfiles after big upgrades
Problem 2: "ModuleNotFoundError even though I installed it"
- Wrong interpreter selected in IDE
- venv not activated in the terminal
- Installed into global Python instead of the venv
# Troubleshooting checklist:
import sys
print("Current Python:", sys.executable)
print()
print("Terminal commands to check:")
print("which python # or 'where python' on Windows")
print("which pip # or 'where pip' on Windows")
print()
print("In VS Code: select interpreter pointing to .venv/bin/python")
# ✅ Expected output (after a first line naming your own Python's path):
# Terminal commands to check:
# which python # or 'where python' on Windows
# which pip # or 'where pip' on Windows
#
# In VS Code: select interpreter pointing to .venv/bin/pythonProblem 3: "pip install works locally but fails in CI"
- OS differences (Linux vs Windows)
- Missing system libraries (for packages with C extensions)
- Extra private indexes not configured in CI
- Use the same Python version as CI locally (e.g. via pyenv)
- Install system packages in CI (e.g. libpq-dev, build-essential)
- Make sure CI has the same PIP_INDEX_URL / extra-index-url
📐 26. Structuring Requirements for Long-Term Maintainability
A common and very maintainable pattern:
# Maintainable requirements structure:
structure = '''
requirements/
├─ base.in
├─ dev.in
├─ prod.in
├─ base.txt
├─ dev.txt
└─ prod.txt
'''
print(structure)
# ✅ Expected output:
#
# requirements/
# ├─ base.in
# ├─ dev.in
# ├─ prod.in
# ├─ base.txt
# ├─ dev.txt
# └─ prod.txt- A clear separation of intent (*.in) vs exact resolved versions (*.txt)
- Easy upgrades by editing .in and re-compiling
- Stable installs on all machines/environments
🧠 27. Mental Models to Keep Your Environments Sane
A few simple rules keep everything under control:
- One project → one virtual environment Don't reuse venvs across unrelated projects.
- Never install app dependencies globally Global Python is for tooling at most, not for full stacks.
- Pin versions for anything serious Experiments can be loose, but production/teaching projects should be pinned.
- Document setup steps once, reuse everywhere A short "Setup" section in your README saves hours of debugging.
- Treat requirements.txt or poetry.lock as part of your code They're versions of your environment — commit them and keep them updated.
✅ 28. Quick Checklist for a "Professional" Environment Setup
If you can answer yes to these, your dependency game is solid:
- Does the project have a .venv and ignore it in .gitignore?
- Are dependencies pinned somewhere (requirements.txt or lockfile)?
- Is there a clear separation between runtime deps and dev/test tools?
- Can someone new set up the project using only the README?
- Can CI recreate the environment from scratch and pass tests?
- Do you avoid installing project dependencies globally?
If any are "no", that's the next thing to improve.
🎓 Final Summary
You've now mastered professional Python dependency management and virtual environments.
- Create isolated, reproducible Python environments
- Manage dependencies with requirements.txt and modern tools
- Structure multi-environment projects (dev/test/prod)
- Debug common dependency issues
- Integrate virtual environments with Docker and CI/CD
- Follow security best practices for supply-chain safety
These skills are essential for building production-ready Python applications used by professional teams worldwide.
🎯 Mini Challenge: The "Works On My Machine" Detector
This is the tool you wish you'd had the last time a teammate's environment behaved differently from yours. You get two lists: what requirements.txt says should be installed, and what pip freeze says actually is. Report every difference.
- Turn each list of name==version strings into a dictionary of name → version.
- For every required package: if it isn't installed, it's MISSING; if the version differs, it's WRONG.
- For every installed package not in requirements, it's EXTRA.
- Finish with a count and the command that fixes it.
The starter block is a comment outline only — no logic is filled in. Match the print formats shown in the comments and your output will match the expected output exactly.
# 🎯 MINI-CHALLENGE: spot the drift between requirements.txt and pip freeze
required_lines = ["fastapi==0.115.0", "uvicorn==0.32.0", "pydantic==2.9.0", "requests==2.32.3"]
frozen_lines = ["fastapi==0.115.0", "uvicorn==0.30.1", "pydantic==2.9.0", "rich==13.9.2"]
# 1. Write a helper that turns a list like ["fastapi==0.115.0"] into {"fastapi": "0.115.0"}
# Hint: "fastapi==0.115.0".partition("==") gives ('fastapi', '==', '0.115.0')
# 2. Build two dictionaries: required and installed
# 3. Keep a counter called problems, starting at 0
# 4. Loop over required. For each package:
# not installed at all -> print(f"MISSING {name} (need {version})")
# installed, wrong ver -> print(f"WRONG {name} has {installed[name]}, needs {version}")
# Add 1 to problems each time you print.
# 5. Loop over installed. Anything not in required:
# print(f"EXTRA {name} {version} is not in requirements.txt")
# Add 1 to problems each time.
# 6. Print a blank line, then:
# if problems == 0 -> "Environment matches requirements.txt."
# otherwise -> f"{problems} problems found - run: pip install -r requirements.txt"
# your code here
# ✅ Expected output:
# WRONG uvicorn has 0.30.1, needs 0.32.0
# MISSING requests (need 2.32.3)
# EXTRA rich 13.9.2 is not in requirements.txt
#
# 3 problems found - run: pip install -r requirements.txt📋 Quick Reference — Virtual Environments
| Command | What it does |
|---|---|
| python -m venv .venv | Create a virtual environment |
| source .venv/bin/activate | Activate (Linux/macOS) |
| .venv\Scripts\activate | Activate (Windows) |
| pip freeze > requirements.txt | Save installed packages |
| pip install -r requirements.txt | Install from requirements file |
🎉 Great work! You've completed this lesson.
You can now create isolated environments, manage dependencies cleanly, and use modern tools like Poetry and pyenv.
Practice quiz
What is the standard-library command to create a virtual environment named .venv?
- pip install venv .venv
- python -m virtualenv create
- python -m venv .venv
- venv new .venv
Answer: python -m venv .venv. venv ships with Python; 'python -m venv .venv' creates a .venv/ folder containing its own interpreter and site-packages.
On macOS/Linux, how do you activate a virtual environment in .venv?
- source .venv/bin/activate
- .venv/Scripts/Activate.ps1
- activate .venv
- python -m venv activate
Answer: source .venv/bin/activate. On Unix shells you source the activate script: 'source .venv/bin/activate'. The PowerShell .ps1 form is for Windows.
What is the main reason to install packages with 'python -m pip' instead of just 'pip'?
- It installs packages faster
- It automatically pins versions
- It is the only way to use requirements.txt
- It guarantees you use the pip tied to that specific Python interpreter
Answer: It guarantees you use the pip tied to that specific Python interpreter. 'python -m pip' guarantees you invoke the pip bound to that interpreter, avoiding 'wrong pip' issues across multiple Pythons.
Which command captures your installed packages and versions into a requirements file?
- pip list > requirements.txt
- pip freeze > requirements.txt
- pip export requirements.txt
- pip save requirements.txt
Answer: pip freeze > requirements.txt. 'pip freeze' prints installed packages with exact versions in requirements format; redirecting it writes requirements.txt.
What does the version specifier '==1.4.3' mean in a requirements file?
- Exactly version 1.4.3 only
- Any 1.x version
- Version 1.4.3 or newer
- Compatible release, 1.4.x only
Answer: Exactly version 1.4.3 only. '==1.4.3' pins the exact version — the safest choice for deployed production apps.
Why should the .venv/ folder be added to .gitignore?
- Because Git cannot store binary files
- Because Git would corrupt the interpreter
- Because the environment is large, machine-specific, and recreatable from requirements.txt
- Because pip refuses to run inside a Git repo
Answer: Because the environment is large, machine-specific, and recreatable from requirements.txt. The venv is rebuildable from requirements.txt and is OS/path-specific, so you never commit it — you commit the requirements instead.
Which tool is recommended for installing global Python CLI tools like httpie or black in isolation?
- pip
- pipx
- venv
- poetry
Answer: pipx. pipx installs each CLI tool in its own isolated environment and exposes it on your PATH, keeping tools separate from project deps.
In a pip-tools workflow, what is the role of a requirements.in file?
- It is the fully pinned lockfile
- It lists only dev tools
- It is an alias for the activate script
- It holds high-level human-friendly dependencies that get compiled into a pinned requirements.txt
Answer: It holds high-level human-friendly dependencies that get compiled into a pinned requirements.txt. requirements.in lists what you WANT; 'pip-compile' resolves and pins everything (including sub-deps) into requirements.txt.
A teammate gets ModuleNotFoundError for a package you installed. What is the most likely cause?
- Python itself is broken
- The virtual environment wasn't activated or the wrong interpreter is selected
- The package no longer exists on PyPI
- requirements.txt must be deleted
Answer: The virtual environment wasn't activated or the wrong interpreter is selected. Usually the venv isn't activated, or the IDE points at a different interpreter, so the install went to the wrong Python.
Why are production dependencies best declared with pinned (==) versions?
- Pinned versions make installs slower but smaller
- Pinned versions auto-upgrade on deploy
- Pinned versions ensure reproducible, identical installs across every machine
- Pinned versions are required by venv
Answer: Pinned versions ensure reproducible, identical installs across every machine. Exact pins give reproducible builds: every machine and CI run installs the same versions, eliminating 'works on my laptop' drift.
Continue this course
- Previous: Building Command-Line Tools with argparse & Typer
- Next: Packaging & Publishing Python Libraries to PyPI — Package your code and publish it so others can pip install it
- Quick reference: Python cheat sheet › Environment & Tooling
- From the blog: 10 Python Tips Every Beginner Should Know