Python 项目中的 pyproject.toml 是什么?打包、依赖和工具配置应该怎么管理?
简化版
pyproject.toml 是现代 Python 项目的核心配置文件,用来声明构建系统、项目元数据、依赖,以及 black、ruff、mypy、pytest 等工具配置。它逐步替代过去分散的 setup.py、setup.cfg、requirements.txt 和各种工具配置文件,让项目更标准、更可复现。
详细版
pyproject.toml 最早由 PEP 518 引入,用来声明构建后端,例如 setuptools、flit、hatchling、poetry-core。后来 PEP 621 规范了 [project] 元数据,包括包名、版本、Python 版本要求、依赖、可选依赖、脚本入口等。
一个常见结构是:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "demo"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["fastapi>=0.110"]
[tool.ruff]
line-length = 100
面试回答要分清:pyproject.toml 描述项目和工具配置;锁文件用于固定完整依赖解析结果;requirements.txt 仍可用于部署或兼容旧流程,但现代项目更倾向用 Poetry、uv、PDM、Hatch 等工具围绕 pyproject.toml 管理。
完整版教学
一、为什么 Python 需要 pyproject.toml
过去 Python 项目配置非常分散:打包写 setup.py,静态配置写 setup.cfg,pip 依赖写 requirements.txt,pytest、mypy、flake8 又各有自己的文件。项目一大,新人很难判断哪个文件才是事实来源。
pyproject.toml 的目标是提供一个标准入口。构建工具知道怎么构建包,包管理工具知道项目依赖是什么,代码质量工具也能把配置放到 [tool.xxx] 下。它不是某个单一工具的私有格式,而是 Python 生态共同采用的项目配置中心。
过去:
setup.py + setup.cfg + requirements.txt + mypy.ini + pytest.ini + ...
现在:
pyproject.toml 作为中心
lock file 固定解析结果
这能减少工程混乱,也方便 CI、构建、发布和 IDE 读取统一元数据。
二、build-system 声明了什么
[build-system] 告诉前端构建工具:构建这个项目需要先安装哪些构建依赖,以及调用哪个后端。这里的“前端”可以理解为 pip、build 等工具;“后端”可以是 setuptools、hatchling、flit_core、poetry-core。
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
如果没有这个声明,构建工具只能猜项目怎么构建,旧项目还可能依赖执行 setup.py。有了 PEP 517/518 模型,构建过程更隔离,也更容易复现。
| 构建后端 | 常见特点 | 适合场景 |
|---|---|---|
| setuptools | 兼容性强,历史最久 | 旧项目、复杂扩展 |
| hatchling | 现代轻量 | 新库、新服务 |
| flit_core | 简洁发布纯 Python 包 | 小型库 |
| poetry-core | 配合 Poetry | Poetry 管理项目 |
三、project 元数据怎么写
[project] 是标准化项目元数据的位置。包名、版本、描述、Python 版本、依赖、可选依赖、命令行入口,都可以放在这里。这样构建出来的 wheel/sdist 能携带标准元数据,pip 和包索引也能识别。
[project]
name = "interview-api"
version = "0.1.0"
description = "Interview question API"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.110,<1",
"pydantic>=2,<3",
]
[project.optional-dependencies]
dev = ["pytest", "ruff", "mypy"]
[project.scripts]
interview-api = "interview_api.cli:main"
版本约束要有边界。比如 fastapi>=0.110 太宽,未来大版本破坏兼容时可能出问题;fastapi>=0.110,<1 更稳。工程项目还需要锁文件固定最终解析出的所有间接依赖。
四、依赖声明和锁文件有什么区别
pyproject.toml 里的依赖通常表达“我兼容哪些版本范围”,锁文件表达“这次部署或开发环境实际使用哪些精确版本”。两者目的不同,不能互相替代。
举个数字例子:项目声明 requests>=2.31,<3,今天解析到 requests==2.32.3,它又依赖 urllib3==2.2.2。半年后重新安装,可能解析到 requests==2.33.0 和新的 urllib3。如果没有锁文件,环境就可能漂移。
pyproject.toml:
requests>=2.31,<3
lock file:
requests==2.32.3
urllib3==2.2.2
certifi==2024.x
...
| 文件 | 作用 | 是否适合提交 |
|---|---|---|
pyproject.toml | 声明项目依赖范围和配置 | 必须提交 |
poetry.lock / uv.lock / pdm.lock | 固定完整依赖图 | 应用项目通常提交 |
requirements.txt | pip 安装清单或导出产物 | 视团队流程 |
库项目和应用项目也不同。库通常不强行锁死所有依赖,避免限制使用方;应用为了部署可复现,通常要提交锁文件。
五、工具配置为什么集中到 tool 区
很多工具都支持在 pyproject.toml 的 [tool.xxx] 下读取配置。这样 black、ruff、mypy、pytest 的规则可以跟项目元数据放在一起,CI 和本地开发更一致。
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-q"
[tool.mypy]
python_version = "3.11"
strict = true
这也便于做代码审查:看到一个项目,只要先看 pyproject.toml,就能知道 Python 版本、依赖、测试入口、lint 规则和类型检查强度。缺点是文件可能越来越长,需要保持结构清晰。
六、常见工具链怎么选
现代 Python 包管理工具很多:Poetry、PDM、Hatch、uv、pip-tools 都能围绕 pyproject.toml 工作。选择时要看团队习惯、发布需求、锁文件稳定性、CI 速度和私有源支持。
| 工具 | 重点能力 | 常见选择理由 |
|---|---|---|
| Poetry | 依赖管理、发布、虚拟环境一体化 | 团队想要完整工作流 |
| uv | 速度快,兼容 pip 生态 | CI 加速、现代项目 |
| PDM | PEP 582/现代元数据支持 | 标准化项目管理 |
| Hatch | 构建、环境、发布 | 库开发和多环境测试 |
| pip-tools | 从声明生成锁定 requirements | 保持 pip 工作流 |
工具不是越新越好。团队已有稳定 pip + requirements 流程时,可以渐进引入 pyproject.toml 管工具配置和构建元数据;新项目则可以直接选择一套现代工具链。
七、常见误区与追问
- 误区:有了 pyproject.toml 就不需要锁文件。 pyproject 声明版本范围,锁文件固定完整解析结果,两者解决的问题不同。
- 误区:requirements.txt 已经完全废弃。 它仍常用于部署、Docker 构建或从锁文件导出,只是项目元数据更推荐放到 pyproject。
- 误区:
setup.py不能再用了。 旧项目仍可用 setuptools;只是新项目更推荐声明式配置和 PEP 517 构建。 - 追问:库项目要不要提交锁文件? 库通常关注兼容范围,不一定提交运行锁;应用服务为了可复现部署通常应提交锁文件。
- 追问:
dependencies和optional-dependencies怎么分? 必需运行依赖放前者,测试、开发、数据库适配等可选能力放后者。 - 追问:为什么要写
requires-python? 它能阻止不兼容 Python 版本安装,也帮助解析器选择合适依赖版本。 - 追问:工具配置都放 pyproject 会不会太大? 会,所以要保持分区清晰;但统一入口通常比散落多个文件更易维护。
八、加强记忆
记住 pyproject.toml 的三层角色:build-system 告诉工具怎么构建,project 描述项目是什么和依赖什么,tool.* 收纳测试、格式化、类型检查等工程配置。再把依赖范围和锁文件区分开:范围负责兼容,锁负责复现。面试里能讲清这两点,就不只是会写依赖文件,而是理解现代 Python 工程化的构建和发布链路。