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:

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:

❌ ProblemWhat Happens✅ Virtual Env Solution
Version conflictProject A needs Django 3.2, Project B needs Django 5.0Each project has its own Django version
OS breaksYou update a package the OS depends onSystem Python stays untouched
Dependency chaosCan't remember which packages belong to which projectEach 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:

Two projects can now safely require:

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 project

Activating 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.ps1

Your prompt usually changes to:

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
# Add these to .gitignore:
gitignore_entries = '''
.venv/
__pycache__/
*.pyc
'''
print(".gitignore entries:")
print(gitignore_entries)

# ✅ Expected output:
# .gitignore entries:
#
# .venv/
# __pycache__/
# *.pyc

Never 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 uvicorn

Why python -m pip?

# Run in terminal:
# python -m pip list

print("List installed packages:")
print("python -m pip list")

# ✅ Expected output:
# List installed packages:
# python -m pip list

Avoid 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.txt

This 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.3

Now 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.txt

This is critical for:

🔍 7. Semantic Versioning & Safer Constraints

Not all dependencies need to be fully pinned, but you should understand version ranges:

SyntaxMeaningWhen to Use
==1.4.3Exact version onlyProduction apps (safest)
>=1.4,<2.0Any 1.x versionLibraries you publish
~=1.4Compatible release: 1.4 or newer, but still 1.x (same as >=1.4,<2.0)Balanced approach
~=1.4.0Compatible release: 1.4.0 or newer, but still 1.4.xPatch 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.0

Then 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.txt

Your production environment only installs requirements.txt, keeping it:

🎯 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.txt

This 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:

🌍 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

🧠 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.txt

If the problem disappears, it was likely:

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.0

How to fix it:

🧱 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.txt

With 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
# uvicorn

Compile into a fully pinned lockfile:

# Compile to lockfile:
print("pip-compile requirements.in")

# ✅ Expected output:
# pip-compile requirements.in

It 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:

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.txt

Example:

# 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_engine

2. 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/simple

Best practices:

🛡 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:

✔ 3. Pin versions in production

Floating version ranges can pull in a bad update.

✔ 4. Audit new dependencies

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:

# 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
#     pytest

This ensures your project:

🧠 19. Workflow & Naming Conventions

Clean conventions prevent chaos:

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

Master this, and your projects become:

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.txt

Example 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.md

Anyone 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.txt

and 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.md

Each 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.txt

Shared 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_lib

This 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:
# # - redis

Keep 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 feature

Install 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.txt

Problem 1: "It works on my machine but not on theirs"

Problem 2: "ModuleNotFoundError even though I installed it"

# 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/python

Problem 3: "pip install works locally but fails in CI"

📐 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

🧠 27. Mental Models to Keep Your Environments Sane

A few simple rules keep everything under control:

✅ 28. Quick Checklist for a "Professional" Environment Setup

If you can answer yes to these, your dependency game is solid:

If any are "no", that's the next thing to improve.

🎓 Final Summary

You've now mastered professional Python dependency management and virtual environments.

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.

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

CommandWhat it does
python -m venv .venvCreate a virtual environment
source .venv/bin/activateActivate (Linux/macOS)
.venv\Scripts\activateActivate (Windows)
pip freeze > requirements.txtSave installed packages
pip install -r requirements.txtInstall 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