Nothing in this module is glamorous, and nothing in this curriculum will save you more hours. Every practitioner eventually loses an afternoon to an environment — the import that resolves to the wrong copy, the two packages whose requirements cannot coexist, the “works on my machine” that doesn’t. The losses stop when you understand the three mechanisms underneath: where Python finds code, how the resolver chooses versions, and what a lockfile actually promises. This lesson is those mechanisms, plus the opinionated setup that follows from them.
Mechanism 1: sys.path — where imports actually come from
import numpy triggers a search down an ordered list, sys.path: the
script’s own directory first, then the active environment’s
site-packages, then system locations. Ninety percent of “wrong version”
mysteries are resolved by printing two things:
import numpy, sys
print(numpy.__file__) # which copy won
print(sys.executable) # which python is even running
The classic traps are all path-ordering stories: a file named types.py or
tokenize.py in your project shadowing the standard library; a notebook
kernel bound to a different interpreter than your terminal (sys.executable
exposes it instantly); and the editable-install trap — pip install -e
writes a pointer to a directory path into the environment, so when the
project moves or is cloned twice, the environment silently imports the OLD
location. Symptoms: your edits “don’t do anything”, or a deleted bug keeps
reproducing. Diagnosis is always __file__; cure is reinstalling the
editable in the environment that runs. (This platform’s own repo history
includes exactly this afternoon. Everyone’s does.)
Mechanism 2: the resolver — dependency solving is real work
pip install torch must choose versions for torch and its entire
dependency closure such that every package’s declared constraints are
simultaneously satisfiable — a constraint-satisfaction problem (NP-hard in
general, usually easy in practice). Understanding three consequences
prevents most fights:
- Order used to matter, and state still does. Installing packages one by one into a long-lived environment accumulates constraints the resolver never saw together; two individually-fine installs can leave a broken pair. The cure is declaring everything in one file and resolving in one shot — which is also why “just pip install it into base conda” degrades into archaeology within a year.
- Version conflicts are usually honest.
package A needs numpy<2, package B needs numpy>=2has no solution; the resolver’s error text names the culprits. The fix is a policy decision (upgrade A, pin B, or isolate), not a magic flag. - Python version is a constraint too — much of the ecosystem publishes wheels per Python minor version, and a “no matching distribution” error often just means your interpreter is too new/old for this package’s wheels, especially in the months after a Python release.
Mechanism 3: lockfiles — the reproducibility contract
Distinguish two files with different jobs. Declared dependencies
(pyproject.toml): what you need, loosely — torch>=2.2. The
lockfile (uv.lock, requirements.txt with == and hashes): what you
got, exactly — every transitive package, pinned, hashed. The first is for
humans and future resolution; the second is the contract that a colleague,
CI, or you-in-six-months can reconstruct the same environment bit for bit.
“Requirements files” that mix the two jobs (loose pins, hand-edited, no
transitive closure) provide neither flexibility nor reproducibility — the
worst point on the curve. Modern tooling generates the lock from the
declaration; you edit only the declaration.
The tools, without tribalism
- venv + pip: standard library, universal, no magic. Slow resolver, no lockfile story of its own — fine for small projects.
- uv: the current best default — a Rust-speed drop-in that resolves and
installs 10–100× faster, manages Python versions, and produces a real
cross-platform lockfile (
uv syncreconstructs exactly). This curriculum’s recommendation for new work. - conda/mamba: solves a DIFFERENT problem — it packages non-Python binaries (CUDA toolkits, MKL, geospatial C libraries). If your stack needs system libraries pip can’t provide, conda earns its complexity; in the pip-wheel era (torch ships CUDA in its wheels now) that need is rarer than folklore suggests. Avoid mixing pip and conda installs in one environment beyond the documented pattern (conda for binaries first, pip last, never alternate).
- Docker: the heavyweight contract — locks the OS too. It is the deployment story (DevOps track, ahead) more than the daily-dev story.
The opinionated setup, in five lines:
uv init myproj && cd myproj # pyproject.toml scaffold
uv add torch numpy pandas # declare; uv resolves + locks in one shot
uv add --dev pytest ruff # dev-only deps, separated
uv run python train.py # always runs IN the project env — no activate dance
uv sync # colleague/CI: reproduce the lock exactly
Habits that compound: one environment per project, never shared, cheap
to delete and rebuild (the environment is disposable; the lockfile is the
asset — commit it); python -m pip over bare pip when debugging (kills
the which-pip ambiguity); and when anything smells wrong, the two-line
__file__/sys.executable incantation before any deeper theory.
Exercises
Work these before the next lesson
- Reproduce the shadowing bug: create
tokenize.pycontainingx = 1in a fresh directory and runpython -c “import pdb”there. Explain the traceback via sys.path order, then fix it two different ways.Solution
Worked solutions are part of Premium — unlock all of them for £5/month →
- 5 more exercises — each with a worked solution — are part of Premium. Unlock everything for £5/month →
References
- uv documentation — astral.sh/uv; the resolver notes are unusually readable.
- Python Packaging User Guide — packaging.python.org, the authoritative glossary for wheel/sdist/editable semantics.
- B. Cannon, “How virtual environments work” — the sys.path mechanics from a CPython core dev.