You open an old project. The code works — maybe. You run pip install -r requirements.txt
, and your terminal starts vomiting out dependencies that look like they're from 2015.
What are half of these packages? Did you even use them? Is six
still a thing?
Now you're staring at a wall of pinned versions and have no clue what's actually important. You hesitate before touching anything — what if removing python-dateutil
somehow breaks your ETL scripts?
Welcome to dependency hell.
If you've been working with Python, you've been here before. We all have. You inherit a repo, or return to one of your own after a year, and now you're doing detective work on your own dependencies. Was this package actually needed? Was it added for a long-dead feature? Was it a side effect of installing jupyter
on a Tuesday afternoon?
Technically, it's functional. Practically, it's a mess.
💡 Pip is still widely used, but you might want to consider more powerful tools like Poetry or uv. They handle private repos, optional and dev dependencies, packaging, environments — the whole buffet.
What requirements.txt
Is and Isn't
At first glance, requirements.txt
looks like your source of truth for the project's dependencies. And that's... sort of true.
But it's also definitely not.
requirements.txt
is not some sacred manifest of your application's actual needs. It's just a snapshot — a list of whatever happened to be installed in your environment at the time you ran the freeze
command.
And unless you're extremely disciplined (spoiler: most teams aren't), that file ends up packed with junk from every experiment, tool, and tutorial detour you've touched in the last six months.
The default behavior most teams fall into is this:
pip freeze > requirements.txt
But now you've frozen everything — not just your app's direct dependencies, but also the stuff that got pulled in because you once installed a linter or some random plugin that dragged five different date libraries along with it.
For example, you can end up with a requirements.txt
that looks like this:
# requirements.txt
numpy==1.17.4
pandas==0.24.2
python-dateutil==2.8.1
pytz==2019.3
six==1.13.0
Which of these dependencies were actually your idea?
If you guessed "just pandas", you hit the nail on the head.
You didn't need six
. Your app didn't import pytz
. And you have no memory of ever touching python-dateutil
directly. So, why are they here? Because pandas
needed them. And pip freeze
doesn't know which library is a direct or transitive dependency — it just dumps the whole dependency graph into it.
💡 Per good advice from 12-factor app principles, it's good practice to pin versions for repeatable installs — but that doesn't mean
requirements.txt
should become your lockfile graveyard.
So if requirements.txt
isn't the right place to pin everything... what is?
Allow me to introduce you to something criminally underused:
Meet constraints.txt
Alright, so as we just covered requirements.txt
is a messy snapshot. But what if we could separate intent from implementation?
What if instead of the "snapshot" file, you had one file that says "here's what I actually depend on" and another that says "here's exactly how I want it pinned"?
Turns out, pip has had support for this the whole time. Since v7.1, we've had the --constraint
flag available, and yet almost no one uses it.
Let's fix that.
Think of it like this:
requirements.txt
: what you intentionally useconstraints.txt
: what version everything should be
It looks like this:
python -m pip install -r requirements.txt -c constraints.txt
Simple, right? With this setup, your requirements.txt
becomes a clean, minimalist list of top-level dependencies — the ones you meant to include.
# requirements.txt
--constraint constraints.txt
pandas
And constraints.txt
carries all the frozen libs, including the transitive stuff:
# constraints.txt
numpy==1.17.4
python-dateutil==2.8.1
pytz==2019.3
six==1.13.0
Why this system works great:
Your
requirements.txt
stays readable and intentionalYou still lock every version in the graph — without pretending you wrote them all by hand
You get full traceability into why something's installed
💡 Can
constraints.txt
replacerequirements.txt
? Nope. Constraints don't say what to install — just which versions are allowed. They're flat lists without dependency context. For full graphs and metadata, you'd need something likepoetry.lock
orPipfile.lock
. Think ofconstraints.txt
as a version gatekeeper, not a dependency manifest.
Want to upgrade a library? Edit constraints.txt
. Want to debug something weird? You've got the full graph. Want to wipe and re-freeze? No sweat.
And yeah — go ahead and embed the constraint in every requirements*.txt
you maintain:
# requirements.txt
--constraint constraints.txt
flake8
black
pytest
Now you've got control without clutter.
You're getting an exclusive preview of my latest article! Want to dive deeper? The full story is just a click away: