1.3.12 · D3Python Intermediate

Worked examples — Virtual environments — venv, pip, requirements.txt

2,509 words11 min readBack to topic

This page walks through every kind of situation virtual environments throw at you: fresh builds, broken activations, version pins, the weird edge cases (empty files, wrong Python, missing env), and an exam twist. First we map the whole territory, then we hit every cell with a fully worked example.

If any word here feels new — venv, pip, PATH, site-packages — it is fully built in the parent note. This page assumes you have met those and now wants you to become unshakeable across all cases.


The scenario matrix

Think of every venv problem as landing in one of these cells. The goal of the worked examples below is that after them, you never meet a cell you haven't already seen solved.

Cell Case class The question it tests Example
A Fresh build (the happy path) Can you create → activate → install → freeze? Ex 1
B Reproduce from a file Can you rebuild someone else's exact env? Ex 2
C Version conflict (two projects) Two Djangos at once — the core "why" Ex 3
D Wrong interpreter You built the env with the wrong Python Ex 4
E Degenerate: empty / missing file requirements.txt empty or absent Ex 5
F Pinned vs loose (== vs >=) Reading a requirements file & predicting installs Ex 6
G Not activated (the silent trap) Install "worked" but went to the wrong place Ex 7
H Word problem (real world) Deploy identical env on a server Ex 8
I Exam twist Trace sys.pathwhy isolation happens Ex 9
Figure — Virtual environments — venv, pip, requirements.txt

Cell A — Fresh build (happy path)

Forecast: Guess before reading — after pip install requests, where does the requests folder physically land? System Python, or somewhere inside your project?

Steps:

  1. mkdir weather && cd weather Why this step? One folder = one project = one env. This keeps each project's toy box separate (parent-note picture).

  2. python -m venv .venv Why this step? The -m venv runs the built-in venv module to build an isolated interpreter folder. .venv is the conventional hidden name so it doesn't clutter listings.

  3. source .venv/bin/activate Why this step? Activation edits the shell's PATH so python and pip now resolve to the copies inside .venv. Your prompt changes to (.venv).

  4. pip install requests Why this step? Because the active pip is the env's pip, requests lands in weather/.venv/lib/python3.11/site-packages/, not system Python.

  5. pip freeze > requirements.txt Why this step? freeze prints every installed package with its exact version; > writes that to a file — the reproducible recipe.

Verify: Run which python → it must print a path ending in weather/.venv/bin/python. And cat requirements.txt shows something like:

certifi==2024.7.4
charset-normalizer==3.3.2
idna==3.7
requests==2.32.3
urllib3==2.2.2

Notice: you asked for 1 package (requests) but got 5 lines. The extra 4 are transitive dependencies — packages requests itself needs. Count check: 1 requested + 4 transitive = 5 lines.


Cell B — Reproduce from a file

Forecast: Will git clone give you the packages, or only the list of packages?

Steps:

  1. git clone https://example/cool-app && cd cool-app Why this step? You receive the code and the recipe file. The env is gitignored, so you get the shopping list, not the groceries.

  2. python -m venv .venv Why this step? The env is disposable and local — you always rebuild, never download it.

  3. source .venv/bin/activate Why this step? Point pip at this fresh empty env before installing.

  4. pip install -r requirements.txt Why this step? -r means "read this file line by line and install each pinned version". Same versions ⇒ same behaviour as the author.

Verify: pip freeze on your machine should print a file identical to the teammate's requirements.txt (same lines, same order after sorting). If they match, reproduction succeeded.


Cell C — Version conflict (the whole reason venv exists)

Forecast: With global install only, how many Django versions can coexist? With venvs, how many?

Steps:

  1. Build A's env and pin its Django:

    cd projectA && python -m venv .venv && source .venv/bin/activate
    pip install Django==3.2
    deactivate

    Why this step? A's site-packages now holds only 3.2, sealed off.

  2. Build B's env separately:

    cd ../projectB && python -m venv .venv && source .venv/bin/activate
    pip install Django==4.2

    Why this step? B's site-packages holds 4.2. Different folder ⇒ no collision.

  3. Check versions per env:

    • In A: python -c "import django; print(django.VERSION[:2])"(3, 2)
    • In B: python -c "import django; print(django.VERSION[:2])"(4, 2)

Verify: Globally you could hold 1 version at a time (installing 4.2 overwrites 3.2). With venvs you hold 2 simultaneously — one per env. That difference (1 → 2) is exactly the dependency-hell problem being solved.

Figure — Virtual environments — venv, pip, requirements.txt

Cell D — Wrong interpreter

Forecast: Does python -m venv download a Python version, or copy the one you invoked?

Steps:

  1. Diagnose: .venv/bin/python --versionPython 3.9.6. Why this step? The env's Python version is frozen to whatever python pointed to at creation time. venv copies/links; it never downloads a new interpreter.

  2. Delete the wrong env: rm -rf .venv Why this step? Envs are disposable. You don't "upgrade" an env's Python — you rebuild.

  3. Recreate with the explicit version: python3.11 -m venv .venv Why this step? Naming python3.11 fixes the interpreter. This is the whole reason we prefer python -m venv over a bare venv command — you control which Python builds it.

  4. source .venv/bin/activate && python --versionPython 3.11.x.

Verify: After the fix, .venv/bin/python --version reports 3.11. If your machine has both, the version number must match the python3.11 you invoked, never a downloaded one.


Cell E — Degenerate input: empty / missing file

Forecast: Empty file → error, or silently install nothing? Missing file → error, or silently install nothing?

Steps:

  1. Case (i), empty file: pip install -r requirements.txt Why this step? pip reads zero lines, so it installs zero packages and exits successfully (exit code 0). No error — an empty recipe is a valid recipe for "nothing".

  2. Case (ii), file absent: pip install -r requirements.txt Why this step? pip cannot open a file that isn't there, so it errors with Could not open requirements file and a non-zero exit code.

  3. Distinguish them: an env built from case (i) has only the base packages (pip, setuptools); case (ii) never even starts installing.

Verify: Number of packages installed in case (i) = 0. Exit code in case (i) = 0; exit code in case (ii) ≠ 0 (it's 1). These two degenerate inputs give different outcomes — empty ≠ missing.


Cell F — Pinned vs loose (== vs >=)

Forecast: Which of the two lines is guaranteed to give the same version next month?

Steps:

  1. Read flask==3.0.3. Why this step? == is an exact pin. pip must install precisely 3.0.3, today and forever. Result: flask 3.0.3.

  2. Read requests>=2.28. Why this step? >= means "at least this version", so pip installs the newest version that is ≥ 2.28. Today that's 2.32.3. Tomorrow, if 2.33.0 releases, this line would install 2.33.0 instead.

  3. Classify: pinned line = reproducible; loose line = "give me compatible latest".

Verify: Today's result → flask 3.0.3, requests 2.32.3. Stability count: exactly 1 of the 2 lines (flask==3.0.3) is guaranteed identical next month; the >= line may drift. See Semantic versioning for how >=, ~=, and ^ bounds are chosen.


Cell G — The silent trap (not activated)

Forecast: If the prompt did not show (.venv) when you installed, whose site-packages got rich?

Steps:

  1. Check activation: prompt shows plain $, not (.venv) $. Why this step? No (.venv) means the shell PATH was never redirected — pip is the system pip.

  2. Confirm: which pip/usr/bin/pip (system), not .../project/.venv/bin/pip. Why this step? This proves the install went into system Python's site-packages, outside your env.

  3. Fix: activate, then reinstall.

    source .venv/bin/activate
    pip install rich

    Why this step? Now pip is the env's pip, so rich lands in the env, where your project's python looks.

  4. Retry: python -c "import rich; print('ok')"ok.

Verify: Before fix, import rich fails inside the env because the env's site-packages had it in 0 places. After activating and installing, it appears in exactly 1 place (the env), and the import succeeds. Always confirm (.venv) and which pip before installing.


Cell H — Word problem (real-world deploy)

Forecast: Should you copy your Mac's .venv/ folder to the server, or do something else?

Steps:

  1. On your Mac: pip freeze > requirements.txt and commit it. Why this step? Freeze captures the exact version of every package (direct + transitive). This is the portable recipe.

  2. On the server: python3.11 -m venv .venv && source .venv/bin/activate Why this step? Build a fresh Linux-native env. Never copy the Mac .venv/ — its binaries are compiled for macOS and won't run on Linux.

  3. pip install -r requirements.txt Why this step? Installs the identical pinned versions, but compiled for the server's OS. Same versions + native binaries = same behaviour, no crash.

Verify: After step 3, run pip freeze on the server and diff against the committed requirements.txt — they must be identical. Copying the Mac .venv/ would have failed because binaries are OS-specific; rebuilding from the recipe works. For fully deterministic deploys see Reproducible builds and Docker and .gitignore best practices (why .venv/ stays out of git).


Cell I — Exam twist: trace why isolation happens

Forecast: What single list does Python search when you write import X, and what changes it?

Steps:

  1. Define the tool. sys.path is the ordered list of folders Python searches, top to bottom, to satisfy an import. Why this tool? Because imports are the only way packages become usable — so isolation must show up here or nowhere.

  2. Before activation, python -c "import sys; print(sys.path)" shows folders ending in the system site-packages, e.g. /usr/lib/python3.11/site-packages. Why this step? System Python builds its path from the system install location.

  3. After source .venv/bin/activate, the same command shows the env's site-packages first, e.g. /proj/.venv/lib/python3.11/site-packages, and the system one is dropped (thanks to pyvenv.cfg with include-system-site-packages = false). Why this step? Activation puts the env's python first on PATH; that interpreter reads pyvenv.cfg and builds a path pointing at the env's packages, not the system's.

  4. Conclusion: because import only ever searches the active sys.path, and the env's path excludes system site-packages, installs into the env are invisible to system Python and vice versa.

Verify: The distinguishing fact: after activation, the number of system site-packages entries in sys.path is 0 (when include-system-site-packages = false). That zero is the mechanical reason isolation holds. Details of path construction: Python import system and sys.path. Contrast with heavier tools in Dependency management — poetry, pipenv and the underlying installer in pip and PyPI.

Figure — Virtual environments — venv, pip, requirements.txt

Recall Did every cell get covered?

Fresh build (A), reproduce (B), version conflict (C), wrong Python (D), empty vs missing file (E), pinned vs loose (F), silent not-activated trap (G), real-world deploy (H), exam sys.path trace (I). Nine cells, nine examples.

Which cell tests the degenerate empty-input case?
Cell E — empty vs missing requirements.txt (empty installs 0 packages, exit 0; missing errors, exit non-zero).
Why can't you copy a .venv/ from Mac to a Linux server?
The binaries are compiled per-OS; rebuild from requirements.txt on the server instead.
After activation with include-system-site-packages = false, how many system site-packages entries are in sys.path?
Zero — that is the mechanical reason isolation holds.
In requests>=2.28, what version installs if 2.32.3 is newest?
2.32.3 — >= installs the newest version that satisfies the bound.