pip Version Specifiers — What `==`, `>=`, and `~=` Actually Commit You To

pip Version Specifiers — What ==, >=, and ~= Actually Commit You To

pip 版本说明符 —— ==、>= 和 ~= 到底意味着什么

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.

任何写过 requirements.txt 文件的人,可能都在包名后的那一行停顿过。是留空、用 ==3.0.0 锁定版本、用 >=3.0.0 设置下限,还是用 ~=3.0.0 折中处理?它们看起来很相似,但每一个对于未来安装什么版本都做出了完全不同的承诺。本文将介绍 pip 的版本说明符语法,探讨我们自己的工具为何选择了特定的风格,以及这种选择所带来的权衡。

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?

注:pip 的版本说明符遵循 PEP 440。在包名后编写运算符和版本号,是告诉 pip 哪些版本范围是可以安装的。如果完全不写说明符,pip 会在安装时自动安装最新的发布版本。这相当于在说“任何当前版本都可以”——但六个月后针对同一个 requirements.txt 运行 pip install 时,它会拉取届时生态系统中最新的版本。版本说明符的存在是为了给这种不确定性设定边界:你希望赋予安装程序多大的自由度,来决定“未来”的版本会是什么样?

The main operators, compared

主要运算符对比

SyntaxMeaning
==3.0.0Only this exact version is allowed (fully pinned)
>=3.0.0This version or newer — a floor only
<3.0.0Only versions below this — a ceiling only
!=3.0.0Any version except this one
~=3.0.03.0.0 or newer, but below 3.1.0 (the “compatible release” operator)
语法含义
==3.0.0仅允许此确切版本(完全锁定)
>=3.0.0此版本或更新版本 —— 仅设置下限
<3.0.0仅低于此版本 —— 仅设置上限
!=3.0.0除此版本外的任何版本
~=3.0.03.0.0 或更新版本,但低于 3.1.0(“兼容发布”运算符)

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 >=.

最容易让人困惑的是 ~=。~=3.0.0 是 >=3.0.0, ==3.0.* 的简写——它接受补丁级更新(第三位数字),但一旦次版本号(第二位数字)增加就会被锁定。如果将其缩短为 ~=3.0,容差范围会扩大,从而接受次版本更新,等同于 >=3.0, ==3.*。你写的位数决定了允许范围的粒度——这正是它与普通 >= 的真正区别。

Why our own requirements.txt uses >=

为什么我们自己的 requirements.txt 使用 >=

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

我们桌面维护工具的 requirements.txt 仅为所有六个依赖项指定了下限: 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.

我们选择 >= 而不是完全的 == 锁定,是为了让新版本中的安全补丁和错误修复能够自动应用。这对于 cryptography(加密原语)和 certifi(CA 证书包)尤为重要,因为它们都会定期接收漏洞修复和根证书更新——将两者无限期锁定在旧版本本身就是一种风险。不设置上限遵循同样的逻辑:在未来版本出现之前,没有任何理由排除它们。

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.

这种权衡体现在构建阶段。该工具使用 PyInstaller 打包成可分发二进制文件,命令为 arch -x86_64 pip3 install -r requirements.txt && arch -x86_64 python3 build_app.py。如果相隔数周运行两次完全相同的命令,且完全不改动 requirements.txt,pip 每次都会重新解析“3.0.0 或更新版本”——因此两次构建最终可能打包进完全不同的 flask 或 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.

仅查看 requirements.txt 的差异无法告诉你两次构建是否确实包含了相同的依赖集。当某个 Bug 仅在特定依赖版本上重现,或者次版本更新悄悄改变了行为(例如弃用警告升级为硬错误,或默认值发生变化)时,这种不确定性就会成为真正的问题,而且由于仓库中没有任何改动,这些变化会悄无声息地进入构建版本。

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.

完全的 == 锁定会将“更新依赖”变成一个经过深思熟虑且经过测试的步骤。而使用 >= 时,更新时机是在某人运行 pip install 的那一刻隐式决定的。需要严格可重复性的项目通常通过两层设置来解决这个问题:保留松散的 requirements.txt 来表达意图,同时生成一个由 pip freeze 产生的锁文件(lock file),每次常规安装都从中读取,仅在有意升级依赖时才重新生成该文件。

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.

==、>= 和 ~= 都是在回答同一个根本问题:允许未来的版本偏离你所测试的版本多少?== 以手动更新为代价最大化了可重复性;>= 以无法仅从需求文件判断当前安装的确切版本为代价,自动获取更新。我们选择 >= 主要是为了避免错过加密和证书相关包的更新——但这种选择不可避免地接受了较低的可重复性标准。你选择哪种运算符,实际上是一个设计决策,取决于你的项目更看重保持最新还是保持可重复性。