从“能装上”到“可复现”Python 团队如何正确使用 requirements.txt、锁定文件与依赖分组团队 Python 项目最常见的混乱往往不是代码写错而是环境不一致A 同事本地能跑B 同事pip install -r requirements.txt后报错CI 今天通过明天突然失败生产环境和开发环境看起来装的是“同一批依赖”行为却不一样。问题的根源在于“能装上”只说明依赖解析器找到了一个可用组合“可复现”要求任何人在指定时间、指定平台、指定 Python 版本下都能安装出同一个环境。这两者之间隔着版本范围、传递依赖、平台差异、构建后端、包索引状态和锁定策略。现代 Python 依赖管理里可以把三类文件分清楚类型作用是否应该提交典型使用场景pyproject.toml/requirements.in声明“我需要什么”是人类维护的直接依赖requirements.txtpip 安装清单也常被当作扁平锁定结果通常是部署、CI、传统 pip 工作流pylock.toml/uv.lock/poetry.lock记录完整解析结果是可复现安装、生产部署依赖分组区分 prod/dev/test/docs/lint是本地开发、测试、文档、CIpip 官方文档明确说requirements 文件是给pip install使用的安装项列表但完整语法与 pip 内部细节紧密绑定并不是一个面向所有工具的通用标准。(pip.pypa.io) Python Packaging 规范则把依赖说明符定义为 PEP 508 风格可以指定包名、版本范围、extras、URL、环境标记等。(Python 打包用户指南)一、requirements.txt不要把它误当成“万能真相”很多团队的第一个习惯是pip freezerequirements.txt pipinstall-rrequirements.txt这在小脚本里能用但在团队项目中很容易埋雷。pip freeze记录的是“当前环境已经装了什么”里面可能混入临时调试工具、全局污染包、过期依赖甚至记录了并非项目真正需要的包。更合理的思路是人类维护我直接依赖什么 ↓ 工具解析所有直接依赖 传递依赖 ↓ 锁定输出给 CI / 生产 / 新同事安装例如先写一个最小输入文件# requirements.in fastapi0.110,1.0 uvicorn[standard]0.29,1.0 pydantic2,3再生成锁定版python-mpipinstallpip-tools pip-compile --generate-hashes-orequirements.txt requirements.in生成后的requirements.txt可能类似fastapi0.115.12 \ --hashsha256:... pydantic2.11.4 \ --hashsha256:... starlette0.46.2 \ --hashsha256:... uvicorn0.34.2 \ --hashsha256:...pip-tools文档说明pip-compile可以从pyproject.toml、setup.py、setup.cfg或requirements.in编译出锁定的requirements.txt它还支持--generate-hashes生成哈希校验。(pip-tools.readthedocs.io)(pip-tools.readthedocs.io)一句话原则requirements.in / pyproject.toml人写 requirements.txt工具生成如果团队成员手改生成后的requirements.txt后续升级、审计、回滚都会变得困难。二、锁定文件可复现环境的核心“能装上”和“可复现”之间最大的差距是依赖解析存在很多合法答案。比如你写requests2.30今天解析到requests2.32.3 urllib32.2.2 certifi2024.8.30几个月后可能解析到requests2.33.0 urllib32.3.0 certifi2025.x.x这两次都“能装上”但行为未必完全一致。真正的锁定文件应该记录直接依赖 传递依赖 精确版本 环境标记 Python 版本约束 平台差异 包来源 文件哈希Python Packaging 已经有pylock.toml规范它的目标就是描述 Python 环境中可复现安装所需的依赖。(Python 打包用户指南) pip 也已有实验性的pip lock命令但官方说明其生成结果只保证对当前 Python 版本和平台有效。(pip.pypa.io)所以在实践中可以这样选项目类型推荐方案传统 pip 项目requirements.inpip-compilerequirements.txt现代应用项目pyproject.tomluv.lock/pylock.tomlPython 库pyproject.toml声明宽松范围测试矩阵覆盖多个版本数据科学项目锁 Python 版本、锁依赖、锁系统依赖必要时用容器生产服务必须提交锁定文件CI 和部署都从锁定文件安装一个更稳的生产安装命令pipinstall--require-hashes-rrequirements.txt如果你用pip-tools同步环境时不要只用pip install更推荐pip-sync requirements.txt因为pip install -r requirements.txt不会自动删除环境里多余的包pip-sync会让虚拟环境精确匹配锁定文件。pip-tools文档也强调pip-sync会安装、升级或卸载必要内容使环境反映 requirements 文件。(pip-tools.readthedocs.io)三、依赖分组别让开发工具污染生产环境团队项目通常至少有这些依赖生产运行fastapi、sqlalchemy、redis 测试pytest、coverage 代码质量ruff、mypy 文档mkdocs、sphinx 本地调试ipython、debugpy如果全部塞进一个requirements.txt生产镜像会变大安全扫描会变吵升级风险也更高。现代写法可以放进pyproject.toml[project] name team-api version 0.1.0 requires-python 3.12 dependencies [ fastapi0.110,1.0, uvicorn[standard]0.29,1.0, pydantic2,3, ] [project.optional-dependencies] postgres [ psycopg[binary]3.2,4, ] redis [ redis5,6, ] [dependency-groups] dev [ pytest8,9, coverage[toml]7,8, ruff0.8,1, mypy1.13,2, ] docs [ mkdocs1.6,2, ]这里有两个容易混淆的概念[project.optional-dependencies]是面向用户的 extras。例如你的库支持 PostgreSQL用户可以这样安装pipinstallteam-api[postgres][dependency-groups]是面向项目内部的开发分组。例如测试、lint、文档构建、类型检查。Python Packaging 规范说明依赖组适合 linting、testing 等内部开发用途也适合不构建分发包的脚本集合这些依赖组不会作为构建后包的元数据发布。(Python 打包用户指南)判断标准很简单用户是否需要通过 pip install your-package[xxx] 使用它 是optional-dependencies 否dependency-groups四、一套可直接落地的团队工作流推荐目录结构team-api/ ├── pyproject.toml ├── requirements/ │ ├── prod.txt │ ├── dev.txt │ └── docs.txt ├── src/ ├── tests/ └── .github/workflows/ci.yml1. 固定 Python 版本python--version# Python 3.12.x可以在仓库里加# .python-version 3.12或在 Dockerfile / CI 中明确指定strategy:matrix:python-version:[3.12]2. 生成生产锁定文件使用pip-toolspip-compile\--generate-hashes\-orequirements/prod.txt\pyproject.toml3. 生成开发锁定文件如果使用分层 requirements可以让 dev 受 prod 约束# requirements/dev.in -c prod.txt pytest8,9 coverage[toml]7,8 ruff0.8,1 mypy1.13,2然后cdrequirements pip-compile --generate-hashes-odev.txt dev.inpip-tools文档也给出了 layered requirements 的思路开发依赖可以通过-c requirements.txt受生产依赖约束从而保证 dev 和 prod 中共享依赖版本一致。(pip-tools.readthedocs.io)4. 新同事本地初始化python-mvenv .venvsource.venv/bin/activate python-mpipinstall-Upip pip-tools pip-sync requirements/prod.txt requirements/dev.txtWindows PowerShellpy-3.12-m venv.venv.venv\Scripts\Activate.ps1 python-m pip install-U pip pip-tools pip-sync requirements/prod.txt requirements/dev.txt5. CI 中禁止“漂移安装”name:cion:pull_request:push:branches:[main]jobs:test:runs-on:ubuntu-lateststeps:-uses:actions/checkoutv4-uses:actions/setup-pythonv5with:python-version:3.12-run:python-m pip install-U pip pip-tools-run:pip-sync requirements/prod.txt requirements/dev.txt-run:ruff check .-run:pytest-qCI 的关键不是“装上最新依赖”而是“装出和团队约定一致的环境”。五、为什么“可复现”这么难1. 传递依赖会变化你只写了fastapi但实际会装fastapi ├── starlette ├── pydantic ├── typing-extensions └── anyio任何一个子依赖发布新版本都可能影响最终环境。2. 平台不同wheel 不同同一个包在 macOS、Linux、Windows 上可能安装不同 wheel。带 C 扩展的包如numpy、cryptography、psycopg更容易受到平台、ABI、系统库影响。3. Python 版本不同依赖树不同很多依赖会写环境标记importlib-metadata; python_version 3.10Python 3.9 和 3.12 解析出来的依赖树可能不同。4.pip install不等于同步假设环境里已经有old-debug-tool1.0你运行pipinstall-rrequirements.txt它不会主动删除这个多余包。于是测试可能偷偷依赖了一个锁定文件里没有的包。5. 未锁哈希供应链风险更高只锁版本不锁文件哈希仍然不如锁版本 锁哈希可靠。实际生产项目建议尽量开启 hash checking。六、常见错误与修复方式错误 1直接手写巨大requirements.txt不推荐fastapi0.115.12 starlette0.46.2 anyio4.9.0 sniffio1.3.1 ...更推荐# requirements.in fastapi0.110,1.0然后工具生成完整锁定结果。错误 2生产和开发混在一起不推荐fastapi pytest ruff mkdocs ipython debugpy推荐拆开requirements/prod.txt requirements/dev.txt requirements/docs.txt错误 3库项目过度锁死依赖如果你写的是库不应该轻易这样dependencies [ requests2.32.3, ]库应该给兼容范围dependencies [ requests2.30,3, ]因为库会被别人的应用集成。真正需要锁死的是“最终应用”的运行环境而不是所有可复用库的公开依赖。错误 4升级依赖没有流程推荐设定固定升级节奏pip-compile--upgrade-orequirements/prod.txt pyproject.toml pip-compile--upgrade-orequirements/dev.txt requirements/dev.in pip-sync requirements/prod.txt requirements/dev.txt pytest如果只升级某个包pip-compile --upgrade-package fastapi-orequirements/prod.txt pyproject.tomlpip-compile默认会尽量保留现有锁定结果需要升级全部依赖时使用--upgrade升级单个包时使用--upgrade-package。(pip-tools.readthedocs.io)七、给团队的一份依赖管理约定可以直接放进团队 README## Dependency Policy 1. 不直接手改生成后的 requirements/*.txt。 2. 新增运行时依赖修改 pyproject.toml 的 [project].dependencies。 3. 新增测试、lint、docs 工具修改 [dependency-groups] 或 requirements/*.in。 4. 变更依赖后必须重新生成锁定文件。 5. 本地和 CI 必须使用 sync而不是随意 pip install。 6. 生产环境只安装 prod 锁定文件。 7. 每次依赖升级必须跑完整测试。 8. Python 小版本、基础镜像、系统包也属于环境契约。八、实践结论requirements.txt不是过时文件但它不应该承担所有角色。它适合作为 pip 安装清单也可以作为由工具生成的锁定结果但项目的“意图”更适合写在pyproject.toml或requirements.in中。锁定文件解决的是可复现问题。它把“当前解析器碰巧选了什么”固化下来让新同事、CI、测试环境和生产环境尽可能一致。依赖分组解决的是环境边界问题。生产只需要运行依赖开发才需要测试、lint、类型检查、文档和调试工具。最终建议可以浓缩成一句 Python 最佳实践声明依赖要清晰锁定结果要提交安装环境要同步依赖升级要审查。当团队从“我这里能跑”走向“任何人都能复现”Python 编程的体验会发生质变问题更少发布更稳协作更轻代码也更值得信任。