Anyone who has written a requirements.txt file has probably paused at the line after the package name. Leave it blank, pin it with ==3.0.0, set a floor with >=3.0.0, or split the difference with ~=3.0.0 — they look similar, but each one makes a completely different promise about what gets installed in the future. This post walks through pip’s version specifier syntax, then looks at why our own tool’s requirements.txt picked one particular style, and what that choice trades away.
The question a version specifier is answering
Note: pip’s version specifiers follow PEP 440. Writing an operator and a version number after a package name tells pip which range of versions is acceptable to install.
Leave the specifier off entirely, and pip installs whatever the latest release happens to be at install time. That’s a statement of “any current version is fine” — but running pip install against the same requirements.txt six months later will pull in whatever the ecosystem has moved on to by then. Version specifiers exist to put a boundary around that uncertainty: how much freedom do you want to give the installer over what “the future” is allowed to look like?
The main operators, compared
| Syntax | Meaning |
|---|---|
==3.0.0 |
Only this exact version is allowed (fully pinned) |
>=3.0.0 |
This version or newer — a floor only |
<3.0.0 |
Only versions below this — a ceiling only |
!=3.0.0 |
Any version except this one |
~=3.0.0 |
3.0.0 or newer, but below 3.1.0 (the “compatible release” operator) |
The one that trips people up most is ~=. ~=3.0.0 is shorthand for >=3.0.0, ==3.0.* — it accepts patch-level updates (the third number) but locks out the moment the minor version (the second number) ticks up. Truncate it to ~=3.0 instead, and the tolerance widens to accept minor updates too, becoming the equivalent of >=3.0, ==3.*. How many digits you write changes the granularity of what’s allowed — that’s the real difference from a plain >=.
Why our own requirements.txt uses >=
Our desktop maintenance tool’s requirements.txt specifies all six dependencies with a lower bound only:
flask>=3.0.0
fabric>=3.2.0
playwright>=1.40.0
cryptography>=41.0.0
Pillow>=10.0.0
certifi>=2024.0.0
We chose >= over a full == pin so that security and bug fixes in newer releases get picked up automatically. That matters especially for cryptography (crypto primitives) and certifi (the CA certificate bundle), both of which receive periodic vulnerability fixes and root-certificate updates — pinning either one indefinitely to an old version is itself a risk. Leaving off an upper bound follows the same logic: nothing rules out a future release before it exists.
What >= gives up in exchange — build reproducibility
The trade-off shows up at build time. This tool is packaged into a distributable binary with arch -x86_64 pip3 install -r requirements.txt && arch -x86_64 python3 build_app.py, using PyInstaller. Run that exact command twice, weeks apart, without touching requirements.txt at all, and pip resolves “3.0.0 or newer” fresh each time — so the two builds can end up bundling genuinely different versions of flask or cryptography. Looking at a diff of requirements.txt alone can’t tell you whether two builds actually shipped with the same dependency set.
That uncertainty becomes a real problem when a bug only reproduces on one specific dependency version, or when a minor-version bump quietly changes behavior — a deprecation warning gets promoted to a hard error, a default value flips — and it slips into a build unnoticed because nothing in the repo changed. Full == pinning would turn “update a dependency” into a deliberate, tested step. With >=, the update timing is implicitly decided the moment someone runs pip install.
Projects that need strict reproducibility typically solve this with a two-tier setup: keep the loose requirements.txt for expressing intent, but also generate a lock file from pip freeze that every routine install actually reads from, regenerating it only when a dependency bump is deliberate. Our tool runs with a small team and a mostly static build environment, and dependency updates are infrequent enough that we haven’t introduced that second tier — we run on >= alone for now. If the build environment grows, or subtle version drift between builds starts causing real problems, a lock file is the natural next step.
Takeaway
==, >=, and ~= are all answers to the same underlying question: how much should the future version be allowed to drift from what you tested against? == maximizes reproducibility at the cost of manual updates; >= picks up updates automatically at the cost of no longer being able to tell, from the requirements file alone, exactly what’s installed right now. We picked >= mainly to avoid missing updates to crypto- and certificate-related packages — but that choice is inseparable from accepting a lower bar on reproducibility. Which operator you reach for is really a design decision about whether your project values staying current or staying reproducible more.