4. pyproject.toml入门

在早期的 Python 项目中,打包配置往往散落在多个文件里:setup.py 负责构建逻辑,setup.cfg 存放元数据,requirements.txt 管理依赖,tox.ini、.flake8、mypy.ini 又各自为政。这种碎片化带来一个直接问题:工具越多,配置文件越多,版本之间还容易互相冲突。PEP 518 和 PEP 621 的相继提出,正是为了解决这一局面,而 pyproject.toml 就是它们共同的落地载体。

PEP 518:先解决“构建系统怎么声明”

PEP 518 的核心贡献是引入 [build-system] 表,让项目能够以声明式的方式告诉前端工具(如 pip):“构建这个项目需要哪些依赖,用哪个构建后端。”在它之前,pip 要构建一个源码包,只能先执行 setup.py,而 setup.py 本身可能依赖尚未安装的库,形成“要构建先安装、要安装先构建”的循环。PEP 518 用一段静态声明打破了这个循环:

[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

这三行信息量很大。requires 列出构建该项目所需的包及最低版本,pip 会先在一个隔离环境中安装它们;build-backend 指定真正执行构建的 Python 对象,这里是 setuptools 提供的 build_meta 后端。换成 Hatchling、Flit、PDM 时,只需改这两项,项目源码结构不必跟着变。这就是“构建系统声明”与“项目内容”解耦的价值。

PEP 621:把项目元数据标准化

PEP 518 解决了构建依赖,但项目自身的名称、版本、作者、依赖等信息仍写在 setup.py 或 setup.cfg 里,格式因工具而异。PEP 621 定义了 [project] 表,用统一字段描述项目元数据:

[project]
name = "demo-pkg"
version = "0.1.0"
description = "一个用于演示 pyproject.toml 的示例包"
readme = "README.md"
requires-python = ">=3.9"
license = { text = "MIT" }
authors = [
    { name = "张三", email = "zhangsan@example.com" }
]
dependencies = [
    "requests>=2.28",
    "click>=8.0",
]

[project.optional-dependencies]
dev = ["pytest>=7.0", "ruff>=0.1"]

这些字段是标准化的,任何遵循 PEP 621 的构建后端都能读取。dependencies 声明运行时依赖,optional-dependencies 声明可选依赖组,安装时用 pip install demo-pkg[dev] 即可带上开发工具。相比过去在 setup.py 里写 install_requires,这种写法更接近数据而非代码,工具解析更稳定,也更容易被 IDE 和依赖分析器理解。

PEP 621 只规定“元数据长什么样”,不规定“谁来读”。setuptools、Hatchling、Flit、PDM 都实现了对 [project] 表的支持,因此同一份元数据可以配合不同构建后端使用。

统一入口:三类配置各归其位

把 PEP 518 和 PEP 621 放在一起看,pyproject.toml 的结构就清晰了。它通常包含三类内容:构建系统声明、项目元数据、工具配置。前两类由 PEP 定义,第三类则由各工具自行约定,例如 Ruff、mypy、pytest 都允许把自己的配置写进 [tool.xxx] 表:

[tool.ruff]
line-length = 100
target-version = "py39"

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-q"

[tool.mypy]
python_version = "3.9"
strict = true

这样一来,项目根目录下不再需要 .flake8、mypy.ini、pytest.ini 等一堆文件,工具配置集中在一个 TOML 文件里,阅读和迁移都更方便。需要强调的是,[tool.*] 并非 PEP 621 强制规定,而是各工具主动支持的结果;使用前应确认目标工具版本是否支持从 pyproject.toml 读取配置。

pyproject.toml 的三类配置与 PEP 来源 PEP 518 管构建系统 · PEP 621 管项目元数据 · [tool.*] 管工具配置 PEP 518 构建系统声明 [build-system] requires / build-backend PEP 621 项目元数据 [project] name / version / dependencies 工具约定 工具配置 [tool.ruff] / [tool.mypy] [tool.pytest.ini_options] pyproject.toml 项目配置的统一入口 · TOML 格式 · 人类可读 · 工具可解析 pip / 构建前端 读取 build-system 构建后端 setuptools / hatchling 开发工具 ruff / mypy / pytest

从零写一份可用的 pyproject.toml

假设要创建一个名为 demo-pkg 的包,源码放在 src/demo_pkg/ 下。第一步先写构建系统声明,选择 setuptools 作为后端:

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

第二步写项目元数据。注意 name 是发布名,可以带连字符;实际导入名由包目录决定,这里是 demo_pkg。版本号遵循 PEP 440,0.1.0 表示初始开发版:

[project]
name = "demo-pkg"
version = "0.1.0"
description = "演示 pyproject.toml 的最小项目"
requires-python = ">=3.9"
dependencies = ["requests>=2.28"]

[project.optional-dependencies]
dev = ["pytest>=7.0", "ruff>=0.1"]

第三步按需加入工具配置。例如让 Ruff 使用 100 列宽、目标 Python 3.9,让 pytest 只在 tests/ 下找测试:

[tool.ruff]
line-length = 100
target-version = "py39"

[tool.pytest.ini_options]
testpaths = ["tests"]

写完后可以用 python -m build 验证构建是否成功,产物会出现在 dist/ 目录下,通常是一个 .tar.gz 源码包和一个 .whl 轮子。如果构建报错提示找不到 build-backend,多半是 requires 里的包名或版本写错;如果元数据校验失败,则检查 [project] 中必填字段是否齐全。PEP 621 要求 name 和 version 至少存在其一,若使用动态版本则需在 dynamic 中声明。

不要把 [project] 和 [tool.setuptools] 的职责混淆:前者是标准元数据,后者是 setuptools 特有的包发现配置。例如 [tool.setuptools.packages.find] 用于指定从哪个目录查找包,它不属于 PEP 621 范畴。

迁移与兼容性判断

如果手头已有 setup.py 项目,迁移时可以分步走:先把 install_requires、name、version 等静态信息搬到 [project],保留 setup.py 中无法声明的动态逻辑;确认构建通过后,再逐步删除冗余的 setup.cfg 和工具配置文件。判断某个工具是否支持 pyproject.toml,最直接的方法是查它的文档中是否出现 [tool.工具名] 配置示例,或运行 工具名 --help 查看是否提及 pyproject。对于仍只认旧配置文件的工具,可以暂时保留原文件,两者并不冲突。

理解 pyproject.toml 的关键,是把它看作一个分层入口:PEP 518 定义构建层,PEP 621 定义元数据层,[tool.*] 定义工具层。三层各司其职,又共享同一个文件,这正是它取代碎片化配置的根本原因。