升级到最新版的 Python 3.15 后,知名开发者 Simon Willison 频繁撞上一类令人困惑的依赖报错,并在 2026 年 11 月 1 日写下一条经验法则:只要遇到疑问,先删掉本地的 uv.lock 文件。这个看似立竿见影的排障直觉,恰恰暴露了现代 Python 开发者在面对 uv 锁文件机制与 Rust 扩展生态时的双重认知误区。
那天拦住 Willison 的是一条具体的构建错误。他在本地运行 uv run pytest 时,watchfiles==1.1.1 构建失败,底层 maturin.build_wheel 抛出提示,指出当前配置的 Python 解释器版本(3.15)高于 PyO3 支持的最大版本(3.14)。他把目录里残留的 uv.lock 删掉重跑,测试立刻恢复正常,但这张被随手撕掉的锁文件里,其实藏着一条完整的工具链断案线索。
为什么 watchfiles 1.1.1 会在 Python 3.15 下死于源码编译
要看懂这个报错,得先拆开 watchfiles 的版本演进与元数据机制。
watchfiles 1.1.1 发布于 2025 年 10 月 14 日,当时的更新主要是补齐 Windows 平台下的 Python 3.14 构建,官方预编译二进制包(wheel)只覆盖到 Python 3.9 至 3.14,既没有提供 cp315 的预编译包,也没有开启跨版本兼容的 abi3 特性。直到后续发布的 watchfiles 1.2.0(通过 PR #369),项目才正式加入对 Python 3.15 开发构建的支持,同时放弃了 Python 3.9 并将最低 Rust 版本要求提升至 1.83。
真正让包管理器掉进陷阱的,是 watchfiles 1.1.1 元数据里写着开放式的 requires-python = ">=3.9",且构建后端约束为 maturin>=0.14.16,<2。当 Willison 切换到 Python 3.15 时,解析器读到 >=3.9,便认为 1.1.1 在语法上完全兼容 3.15;发现 PyPI 上没有对应 3.15 的 wheel 包后,uv 顺理成章地回退到本地源码构建(sdist)。
源码构建一启动,底层的 Rust 编译器就接管了现场。watchfiles 1.1.1 依赖的 PyO3 0.26.0 为了防范 CPython 每年大版本更新带来的 C API 变动风险,内置了硬性的解释器版本上限校验,最高只认到 Python 3.14。Python 社区习惯不设上限的元数据声明,与 Rust 工具链宁可报错也不冒险编译的严格上界,就这样在本地构建阶段迎头相撞。
换了 Python 解释器,为什么 uv 还在死磕旧版本
既然 PyPI 上已经有支持 3.15 的 watchfiles 1.2.0,为什么 Willison 运行 uv run pytest 时,uv 没有自动拉取新版?
这涉及 Astral 团队为 uv 设计的锁文件偏好策略。切换本地 Python 版本并不会触发 uv 自动升级 uv.lock 里的所有依赖;只要项目配置里的版本约束没有改变,uv 默认倾向于保留已锁定的版本,以此保证跨环境构建的一致性。
因此,直接删掉 uv.lock 属于推倒重来的粗暴重置。按照 uv 官方文档的设计机制,处理这类局部版本卡死的标准动作是运行 uv lock --upgrade,允许解析器在项目约束范围内重新评估并升级所有包;如果只想定点排障,执行 uv lock --upgrade-package watchfiles 就能在不惊动其他依赖的前提下,单独把卡壳的包推到兼容 3.15 的新版本。
- 建议.升级 Python 大版本后若遇 C 或 Rust 扩展编译失败,优先运行
uv lock --upgrade-package定点升级对应依赖,而非直接删除整个锁文件。
把 uv.lock 塞进 .gitignore,恰恰制造了幽灵缓存
比排障命令更值得讨论的,是为什么 Willison 的本地目录里会躺着一份没人管的旧 uv.lock。
他在博客中解释,自己的大多数项目都是供他人调用的 Python 库(library),为了不对下游使用者显得“不礼貌”(避免锁死别人的依赖版本),他从来不在仓库里包含 uv.lock,而是将其写进 .gitignore。早在 2025 年 8 月 6 日的 Hacker News 讨论中,他就表达过完全相同的观点,认为库项目锁定精确版本不可避免地会破坏其他依赖。
真正把开发者绊倒的,往往不是锁文件本身,而是被藏进 .gitignore 的本地幽灵。
但这一沿自早期工具链的直觉,在现代 Python 打包规范下其实是一个典型的认知误判。当时在 Hacker News 上就有开发者指出,锁文件只作用于库自身的开发环境;uv 的官方文档更是明确建议,即便是库项目也应当将 uv.lock 提交到版本控制中。
原因在于开发态与发布态有着严格的物理隔离。当维护者把库打包成 wheel 或 sdist 发布到 PyPI 时,构建工具只读取 pyproject.toml 里的宽松依赖范围,代码仓库里的 uv.lock 根本不会进包,也绝无可能锁死下游用户。至于如何确保库在低版本依赖下不出错,uv 官方给出的方案是在 CI 中配合 --resolution lowest 或 --resolution lowest-direct 参数,专门测试直接或全部依赖的最低兼容下限。
反过来,把 uv.lock 塞进 .gitignore 却每天使用 uv run pytest,结果就是 uv 依然会在本地磁盘上悄悄生成并复用这份锁文件。因为它被 Git 忽略,开发者在 git status 里看不见它,久而久之就变成了一份无人维护的本地幽灵缓存。等到几个月后升级 Python 解释器时,这份隐身的旧锁文件就会突然跳出来拦路。
随着 pydantic-core、watchfiles 等越来越多高性能基础库转向 Rust 编写并依赖 PyO3 与 maturin,每逢 CPython 年度大版本更新,缺少预编译 wheel 触发源码构建失败的场景还会反复上演。对率先尝鲜 Python 3.15 的开发者来说,与其养成“遇事不决删锁文件”的条件反射,不如把 uv.lock 从 .gitignore 里请出来,让依赖升级重新回到阳光下受控运行。
