Pinning dependencies, so it still runs next year
The file that makes a project reproducible
# requirements.txt
requests==2.32.3
pandas==2.2.2
python-dotenv==1.0.1python3 -m pip install -r requirements.txtWithout this file, "install the packages" means whatever versions happen to be current the day somebody tries, which is not the same set you tested against.
Exact pins, and the ranges you will see
requests==2.32.3 # exactly this
requests>=2.31 # this or newer — resolves differently over time
requests~=2.32.0 # 2.32.x but not 2.33 — compatible-release
requests!=2.30.0 # anything but the broken oneThe convention worth following: applications pin exactly, libraries specify ranges. An application is installed once into a known environment and you want that environment identical everywhere. A library is installed alongside other people's dependencies, and pinning exactly guarantees a conflict with somebody.
pip freeze is not the same as your requirements
python3 -m pip freeze > requirements.txtThis writes every package in the environment, including the ones installed as dependencies of your dependencies — typically 40 lines when you asked for four things. It is a snapshot, not a statement of intent, and six months later nobody can tell which of the 40 you actually need.
The maintainable arrangement is two files: requirements.in listing what you asked for, and a generated requirements.txt with everything pinned. pip-tools (pip-compile) does the generating; uv pip compile does the same thing much faster. poetry and pdm bundle the same idea with project management.
Any of these is fine. Having neither, and a hand-edited freeze, is what breaks.
Transitive dependencies are the ones that bite
You installed pandas. pandas needs numpy. You never named numpy, and a numpy release can still break your program. This is why a lock file records the whole resolved tree, not only your direct choices.
python3 -m pip list --outdated
python3 -m pip show pandas # what it requires, and what requires itUpgrading deliberately
python3 -m pip install --upgrade pandas
pytestUpgrade one thing, run the tests, commit. Upgrading everything at once and finding a failure gives you a bisect over 40 packages. This is the practical payoff of the testing module: a test suite is what makes a dependency upgrade a five-minute job instead of a day of manual checking.
Two commands never to run
sudo pip install requests
pip install --break-system-packages requestsInstalling into the system Python can break tools your operating system depends on, and on some distributions the package manager will overwrite what you did anyway. Use a virtual environment, always, even for a one-file script. The previous lesson covers creating one; this is why the rule has no exceptions.
Security, briefly and honestly
python3 -m pip install pip-audit
pip-auditpip-audit checks your installed versions against a database of known vulnerabilities. It is free, takes seconds, and belongs in CI.
Two risks worth naming. Typosquatting: a package named requsts or python-dateutil2 that exists only to run code on installation. Check the name character by character against the documentation, and look at the project's page before installing something you have not used. A compromised release of a legitimate package, which is rarer and which pinning protects you from until you upgrade — another reason not to upgrade everything blindly.
Installing a package runs code from the internet on your machine, with your permissions. That is not a reason to avoid packages; it is a reason to know what you are installing and to keep the list short.
What breaks two years later, even with pins
Honesty about the limits. A pinned requirements.txt still fails to install eventually, for reasons outside your control:
- an old release withdrawn from the index
- no prebuilt wheel for your newer Python, so it tries to compile and needs a toolchain you do not have
- a transitive dependency that was never pinned resolving to something incompatible
For anything that genuinely must run identically in five years, the answer is a container image or a vendored copy of the packages, not a text file of versions. A requirements.txt gets you months to a couple of years of reproducibility, reliably. It does not get you a decade, and anyone who says otherwise has not tried.
The minimum for any project you share
Four lines in the README: create a virtual environment, activate it, install from requirements, run this command. Plus the Python version you developed against. That is enough for somebody else to get your code running without asking you a question, which is the whole point.
The one thing to keep
Applications pin exact versions and libraries give ranges, and a `pip freeze` snapshot is not the same as a record of what you actually asked for.
Before you move on
A project's requirements.txt says `pandas>=2.0`. It installed cleanly in March and a fresh install in September fails inside code nobody changed. What is the most likely explanation?
Pick the one you would defend. Nobody sees your answer.