pylock.toml 已经不是提案了。PEP 751 在 2025 年 3 月 31 日被接受,状态是 Final,规范正文如今住在 PyPA packaging specs 里持续维护。但把它理解成「Python 终于有官方 lockfile 了」会直接踩坑:它标准化的是一份已经解析完的结果怎么写进文件,不是怎么锁、也不是锁完之后怎么增量更新。最直接的后果是——pip 生成的 pylock.toml 和 uv 导出的 pylock.toml 文件名一样、都完全合法,但前者只能在生成它的那台机器上装,后者跨平台可用,而文件里没有任何字段能让消费方分辨自己拿到的是哪一种。
规范钉死了什么
必填只有三样:lock-version(当前唯一合法值 "1.0")、created-by、[[packages]] 数组。可选的顶层键是 environments、requires-python、extras、dependency-groups、default-groups,外加给工具自留的 [tool]。文件名必须匹配 ^pylock\.([^.]+\.)?toml$——即 pylock.toml 或 pylock.dev.toml,pylock..toml 和 pylock.foo.bar.toml 都非法。
真正决定这个格式性格的是两条规则:
第一,packages.dependencies 是纯审计信息,安装器 MUST NOT 拿它做安装决策。这意味着 pylock.toml 不是一张待遍历的依赖图,而是一张扁平的、已经算完的清单。安装器的工作只剩过滤:校验 lock-version、校验 requires-python、逐个求值 packages.marker、选出源(wheels / sdist / vcs / directory / archive 五选一)、装上。装机时不需要 resolver,这是整个设计的核心收益。
第二,同一个包可以出现多次,但「安装时必须收敛到唯一一条」。多环境就靠这个实现:一条 marker = "sys_platform == 'win32'",一条 marker = "sys_platform != 'win32'"。哈希是强制的(artifact 的 hashes 表 MUST 至少有一项,且 SHOULD 包含一个安全算法),所以任何合法的 pylock.toml 天然是可校验的。
规范故意没管什么
Brett Cannon 在 2025 年 10 月的复盘文章里写得很清楚,这套东西磨了四年,最大的分歧就是「单环境还是多环境」——Astral、Poetry、PDM 三方各有立场,最后的妥协是:格式两种都允许,具体产出哪种由工具自己决定。
于是留下三个洞,每一个在生产里都会咬人:
没有回锁路径。 pylock.toml 只记录解析结果,不记录你原本声明的约束。你无法从一个 pylock.toml 出发「只升级 requests,其它不动」——没有工具支持把它作为重新锁定的输入,因为文件里根本没有直接依赖的意向。它是输出,不是源。
单环境/多环境不可区分。 缺少 environments 键在规范里的含义是「不做环境约束」,而不是「这是单环境文件」。所以一个只在 macOS/CPython 3.14 上有效的文件,和一个覆盖全平台的文件,从结构上完全合法且难以区分。
依赖组的边界会丢。 extras 和 dependency-groups 顶层键加上 marker 扩展本来就是为此设计的,但主流工具目前不写。
同一个文件名,两种东西
这不是推理,是实测。用一个最小项目(click>=8.1 加 uvloop>=0.19; sys_platform != 'win32',dev 组一个 pytest)分别跑 pip 26.2.1 和 uv 0.12.2:
python -m pip lock "click>=8.1" "uvloop>=0.19; sys_platform != 'win32'" -o pylock.toml
uv export --format pylock.toml -o pylock.toml
前者是把当前解释器加平台的解析结果快照下来,后者是把已有的 uv.lock 翻译过去。
pip 那份的 packages 只有两项:click 和 uvloop,各带恰好一个 wheel(uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl),没有 marker,没有 requires-python,顶层键只有 lock-version / created-by / packages。注意 colorama——它是 click 在 Windows 上的依赖,因为锁的时候 marker 求值为假,整条记录被丢掉了。
uv 那份有 requires-python = ">=3.11",colorama 带 marker = "sys_platform == 'win32'" 保留着,uvloop 下挂 30 个 wheel 覆盖 cp311–cp314 的 macOS/manylinux/musllinux(自由线程构建还会再多出一套带 t 的 ABI 标签,见Python 自由线程生产指南),每个 artifact 都带 size 和 upload-time。
两种失败模式截然不同。uvloop 这种「有记录但轮子不匹配」的情况会响亮地失败——把 pip 那份 pylock.toml 拿到 CPython 3.12 环境里(解释器升级时这类 ABI 断裂尤其常见,见Python 3.15 beta4 迁移指南),uv 直接报 Package 'uvloop' can't be installed because it doesn't have a source distribution or wheel for the current platform,并提示只有 cp314 轮子。但 colorama 这种「记录被整条删掉」的情况会安静地成功:在 Windows 上照样装完,退出码 0,然后 click 的彩色输出在运行时坏掉。安装器不会去看 packages.dependencies,所以它根本无从知道少了什么。
pip 自己也知道这个边界,直接把跨平台参数封死了:
ERROR: Platform and interpreter constraints using --python-version, --platform,
--abi, or --implementation, are not supported when selecting requirements from
'pylock.toml'
还有个容易忽略的细节:pip 26.2 给 -r pylock.toml 加了 upload-time 字段支持,让 --uploaded-prior-to(供应链冷却期)能生效。但我实测 pip lock 自己产出的文件里 size 和 upload-time 都是空的——冷却期只对 uv 导出的锁文件有效,对 pip 自己锁的没用。
environments 和「收敛到唯一一条」
这两个字段是多环境锁的关键,也是最容易写错的地方。
environments 是一组 marker 表达式,声明这份锁文件被设计用于哪些环境。规范要求安装器必须确认至少有一条被满足,否则报错。它是一道显式的护栏——但 pip 和 uv 现在都不写这个键,所以护栏实际上是空的。如果你自己生成 pylock.toml(比如从 SBOM 或内部解析器),把它写上:这是唯一能让「这份锁不适用于你的机器」变成安装期硬错误、而不是运行时神秘 bug 的机制。
packages[].marker 则决定了每条记录在什么条件下生效。规则是求值后必须收敛到每个包恰好一条——同一个包的两条记录在同一环境下同时为真就是冲突,安装器必须报错。手写或拼接锁文件时这是最常见的错误来源,pip 26.2 专门改进了这类冲突的报错文案,说明它在真实使用中确实高频。
顺带一提:marker 语法在这里被扩展了,extras 和 dependency_groups 是两个新的集合型变量,配合 in 使用。用 packaging 26.3 验证过:
from packaging.markers import Marker
Marker("'dev' in dependency_groups") # OK
Marker("'test' in extras") # OK
这是规范给「一份文件覆盖多套依赖组」留的门(packaging 25.0,2025-04-19 起支持解析),只是目前 uv 和 pip 导出时都还没走进去。
工具链现状(截至 2026 年 8 月)
pip 是分两步走的:25.1(2025-04-26)加了实验性的 pip lock,整整一年后的 26.1(2026-04-26)才加上实验性的 -r pylock.toml 安装。这个时间差有实际影响——pip 26.0 及以下遇到 -r pylock.toml 会当成 requirements.txt 解析,报出 Invalid requirement: 'lock-version = "1.0"' 这种莫名其妙的错。26.2(2026-07-29)补了 --only-final、upload-time、更好的冲突报错,以及一条安全修复:从 URL 拉取的 pylock.toml 里,解析到锁文件位置之外的 path 会被拒绝,防止远程锁文件指向本地文件系统。最新是 26.2.1(2026-08-04)。
uv 的支持面最广——uv export --format pylock.toml、uv pip compile -o pylock.toml、uv pip sync pylock.toml、uv pip install -r pylock.toml、uv run --with-requirements pylock.toml 都能用,但到 0.12.2(2026-08-05)仍会打印 The --pylock option is experimental and may change without warning。0.12.0(2026-07-28)收紧了校验:packages 数组缺失不再当成空锁(此前 uv pip sync 会因此把环境卸空)、文件名规则强制、artifact 声明了 size 就必须对得上。立场很明确,官方文档写着「uv 的部分功能无法用 pylock.toml 格式表达,因此 uv 在项目接口里会继续使用 uv.lock 格式」。
PDM 是唯一把它当原生锁格式的:2.24.0(2025-04-18)支持导出,2.25.0(2025-06-13)加了 pdm config lock.format pylock,开了之后 pdm lock 直接写 pylock.toml 而不是 pdm.lock,读写闭环。
Poetry 最保守:要到 2.3.0(2026-01-18)配合 poetry-plugin-export 1.10.0 才能 poetry export -f pylock.toml,发布公告里直说「Poetry 尚不能用 pylock.toml 替代 poetry.lock」。
pipenv 提供了独立的 pipenv pylock --generate / --validate,但 vcs、directory、archive 三种源都还在 "Future Enhancements" 里。
最容易被忽略但最该知道的是 packaging 库:26.0(2026-01-20)加入了 packaging.pylock,26.1(2026-04-14)加了 Pylock.select(),26.3(2026-08-03)又补了 prefer_sdist_predicate。也就是说你现在不用自己写解析器:
import tomllib
from packaging.pylock import Pylock
lock = Pylock.from_dict(tomllib.load(open("pylock.toml", "rb")))
for package, artifact in lock.select(dependency_groups=["dev"]):
print(package.name, package.version, artifact.filename)
收敛检查是内建的,重复条目会抛 PylockSelectError。但要看清它的校验边界:URL 和 path 字段原样保存不做合法性检查,相对路径不会相对锁文件解析,哈希算法名和摘要格式不校验,也不下载任何东西——对不对得上真实 artifact 是调用方的事。
该怎么用
别把它当团队的锁文件源。 用 uv 或 Poetry 的团队,仓库里的真相仍然是 uv.lock / poetry.lock(uv 这边工作区怎么切、锁文件怎么校验见uv 工作区与锁文件实战)。换成 pylock.toml 你会丢掉依赖组边界(uv 导出时会把默认组直接摊平成无条件依赖,我实测 --all-groups 也不会写出 dependency-groups 顶层键)、丢掉 conflict 声明、丢掉增量更新能力。PDM 用户是唯一的例外,因为 PDM 真的把它当锁格式在读写。
把它当构建产物。 在 CI 里从原生锁文件导出,随镜像一起发布,用于部署安装、离线/内网安装、SBOM 输入、审计留痕。它的哈希强制和扁平结构正是为这个场景设计的,而且 pip 见到带哈希的需求会自动打开 --require-hashes(26.2 起可用 --no-require-hashes 关掉)。
用命名文件把环境切开。 既然依赖组和平台都可能被摊平,就别指望一个文件通吃:pylock.prod.toml、pylock.ci.toml 各导一份,名字本身就是文档。规范特意允许命名变体,也特意允许服务方先找 pylock.<自己的名字>.toml 再回落到 pylock.toml。
消费方要自己确认可移植性。 如果你要装一份来路不明的 pylock.toml,先看有没有 requires-python、有没有 marker、包的 wheel 数量是不是恒等于 1。三项全中,基本就是某台机器的快照,别拿去跨平台部署。
PEP 751 是个好标准,它把「解析结果」这一层从各家私有格式里解放了出来。但它明确不是「Python 版的 Cargo.lock」——它是可移植的安装清单,不是可维护的依赖真相。分清这两件事,工具链现在的支持进度就够用了。