1.3.12 · D2Python Intermediate

Visual walkthrough — Virtual environments — venv, pip, requirements.txt

1,988 words9 min readBack to topic

This is the deep-dive companion to the parent topic. The parent told you a venv is "PATH manipulation + a config file." Here we derive that claim from absolute zero and draw every step.


Step 1 — What "import" really means: a search through folders

WHAT. When you write import requests, Python does not look inside your computer for a magic file. It has a plain ordered list of folders, and it walks that list top-to-bottom asking each folder: "do you contain requests?" The first folder that says yes wins. That list is called ==sys.path== (see Python import system and sys.path).

WHY this matters. If we can control which folders are on that list, and in what order, we control which copy of a package gets imported. That single idea is the entire foundation of virtual environments. So before anything else, we must see the list.

PICTURE. Below, the yellow box is your running program. The stack of folders on the right is sys.path. Python checks them in order — the red arrow shows the search stopping at the first match.

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

Step 2 — The problem: one global site-packages for everyone

WHAT. Without a venv, there is exactly one site-packages folder shared by every project on the machine. Every pip install dumps into it. Every import reads from it.

WHY it breaks. A folder can only hold one version of Django at a time — a folder is not a magic multi-version container. So if Project A pinned Django==3.2 and you run pip install Django==4.2 for Project B, the second install overwrites the first. Project A now silently imports 4.2 and may crash. This is dependency hell.

PICTURE. Two projects, one shared shelf. The coral arrows show both reaching for the same box — and the box can only hold one label at a time.

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

Step 3 — The fix in one idea: give each project its own folder on sys.path

WHAT. A virtual environment creates a new, empty site-packages folder that belongs to this project only, and arranges for Python to search that folder instead of the global one.

WHY this is enough. From Step 1, imports resolve against whatever site-packages is on sys.path. If Project A's Python searches folder and Project B's Python searches a different folder , then installing Django==4.2 into cannot possibly touch . Isolation falls straight out of "different folder = different search path."

PICTURE. The same two projects, now with separate shelves. Each coral arrow reaches into its own private box — no collision is even possible.

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

Step 4 — What python -m venv .venv builds on disk

WHAT. The command python -m venv .venv creates a folder named .venv containing three things — and nothing that needs downloading (it copies/links the Python you already ran):

Reading the equation term by term:

  • pyvenv.cfg — a tiny text file whose one job is a line like home = /usr/bin telling the env where the real Python lives so it can still find the standard library (you don't get a private copy of os, math, etc.).
  • bin/ (Linux/macOS) or Scripts/ (Windows) — holds a python launcher and a private pip (pip and PyPI) that is pre-wired to install into this env's site-packages.
  • site-packages/empty on creation. This is the private shelf from Step 3.

WHY it's built this way. We want isolation for third-party packages but we do not want to duplicate the whole standard library (wasteful). So the config file borrows the stdlib from base Python, while the fresh empty site-packages gives us a clean slate for installs.

PICTURE. The anatomy of the .venv folder, with each part labelled by the job it does.

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

Step 5 — What activate actually changes: the PATH variable

WHAT. Your shell finds the python command by searching another ordered list of folders — the operating-system ==PATH== variable (different from sys.path, same idea one level up). activate prepends the env's bin/ folder to the front of PATH.

  • .venv/bin at the front — because the shell stops at the first match, putting the env first means typing python now runs the env's python, and pip runs the env's pip.
  • the old entries — still present, just lower priority. Nothing was deleted; the search order was reshuffled.

WHY this is the whole trick. The env's python is configured so that when it boots, its site-packages is on sys.path. So: activate fixes PATH → typing python/pip hits the env copies → those copies read/write the env's site-packages. Two path lists (one for the shell, one for Python) chained together give you full isolation. No magic — just reordering search lists.

PICTURE. Before/after of PATH. The lavender segment .venv/bin slides to the front; the green arrow shows python now resolving there.

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

Step 6 — pip freeze > requirements.txt: recording the shelf

WHAT. After installing, pip freeze reads the env's site-packages, lists every package with its exact version, and > writes that list into requirements.txt.

  • name==version — a pin: == means "exactly this version, no other." This is what makes rebuilds reproducible (see Semantic versioning).
  • "every package" — including transitive dependencies (the packages your packages needed), which is why pip install flask can produce a dozen lines.

WHY. From Step 3, isolation is per-machine. To share the state of a shelf across machines, we don't ship the shelf (it's OS-specific and huge) — we ship the recipe to rebuild it. That's the .txt file. See .gitignore best practices for why .venv/ itself is ignored, and Reproducible builds and Docker for pushing this further.

PICTURE. The env's site-packages on the left; the arrow "freeze" flattening it into a plain text list on the right.

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

Step 7 — Edge & degenerate cases (every scenario shown)

WHAT. Let's walk the corners so you never hit an unshown case:

  1. Empty env, no activationPATH unchanged, python = system Python, installs pollute global site-packages. (This is the "forgot to activate" trap.)
  2. Activated but empty site-packagesimport requests raises ModuleNotFoundError, because the shelf is genuinely empty. Correct behaviour, not a bug.
  3. requirements.txt emptypip install -r requirements.txt does nothing; you get a working env with only the standard library.
  4. Two envs, same package different versions — perfectly fine; different folders (Step 3) means no interaction at all.
  5. deactivate — restores the old PATH (saved when you activated). Reverses Step 5 exactly; nothing is deleted from disk, the shelf still exists.

WHY show these. Each is a place where beginners think "something broke." Every one is just Steps 1–6 running as designed.

PICTURE. A decision map: is the env activated? is the package installed? — leading to what import does in each corner.

Figure — Virtual environments — venv, pip, requirements.txt
Recall Check the corners

New terminal, you type pip install X and forgot to activate — where does X go? ::: Into global/system site-packages, because PATH was never modified. Always confirm the (.venv) prompt (or run which pip) first. Activated env, import requests fails — is the venv broken? ::: No — the shelf is just empty. Run pip install requests. You deactivate — did your installed packages vanish? ::: No. deactivate only restores the old PATH; the .venv/site-packages files stay on disk.


The one-picture summary

Everything above is one chain: import/python follow ordered lists → venv gives each project its own site-packages and reorders those lists so the env wins → freeze records the shelf as a text recipe.

Figure — Virtual environments — venv, pip, requirements.txt
Recall Feynman: the whole walkthrough in plain words

Python finds packages by walking down a list of folders and grabbing the first match — that's sys.path. Normally every project shares one package folder, so two projects wanting different versions of the same tool fight over one shelf and one loses. A virtual environment fixes this by making a brand-new empty shelf (.venv/site-packages) just for one project. The little pyvenv.cfg file lets that project still borrow Python's built-in tools, so the env stays small. activate doesn't do anything mysterious — it just puts the env's folder at the front of the shell's search list (PATH), so typing python and pip now finds the env's copies, which read and write only the env's shelf. Install what you like — it can't touch anyone else. When the shelf is stocked, pip freeze writes down the exact list of what's on it into requirements.txt, a shopping receipt. Ship the receipt, not the shelf: on another machine, pip install -r requirements.txt re-stocks an identical shelf. deactivate just puts the old search list back. Different folders, reordered search lists — that's the entire magic trick.


Active recall

What ordered structure does import requests search?
sys.path, an ordered list of folders; the first one containing the package wins.
Why can't one global site-packages hold Django==3.2 and Django==4.2 at once?
A folder maps each package name to exactly one installed version; installing one overwrites the other.
What three things does python -m venv .venv create?
pyvenv.cfg (points to base Python), bin//Scripts/ (private python + pip), and an empty site-packages/.
What does activate change under the hood?
It prepends .venv/bin to the shell's PATH so python and pip resolve to the env's copies first.
After deactivate, do your installed packages disappear?
No — only PATH is restored; the env folder and its packages stay on disk.
Why ship requirements.txt instead of the .venv/ folder?
The env is large and OS-specific; the text file is a small, portable recipe to rebuild an identical shelf.