pyproject.toml 配置完全手册:从 PEP 518 到现代 Python 项目管理

pyproject.toml 从 PEP 518 到生产实践的深度指南:项目元数据配置、构建系统选型(setuptools/hatch/flit)、依赖声明、可选依赖组、入口点配置、工具配置(pytest/black/mypy)。附带从 setup.py 迁移的完整路径和常见配置模板。

pyproject.toml 是 Python 项目的"身份证"——它告诉构建工具如何打包你的代码,告诉编辑器如何格式化它,告诉 CI 如何测试它。本文从标准演进历史到逐字段解析,让你彻底掌握这份配置文件。


目录

  1. 为什么是 pyproject.toml
  2. 历史演进:从 setup.py 到 PEP 518/621
  3. pyproject.toml 完整结构解析
  4. 实战配置模板
  5. 构建后端对比:setuptools vs hatch vs flit
  6. 依赖管理:从声明到锁定
  7. 工具配置集中化
  8. 从 setup.py 迁移指南
  9. 常见问题与排查

1. 为什么是 pyproject.toml

pyproject.toml 出现之前,Python 项目配置散落在各种文件中:

# 以前的状态(混乱)
myproject/
├─ setup.py          # 打包配置(命令式代码)
├─ setup.cfg         # 静态打包配置
├─ requirements.txt  # 依赖
├─ requirements-dev.txt  # 开发依赖
├─ MANIFEST.in       # 包含文件规则
├─ tox.ini           # 测试环境配置
├─ .flake8           # 代码风格配置
├─ pytest.ini        # 测试框架配置
├─ .pylintrc         # 代码质量配置
└─ ...

pyproject.toml 的目标:一个文件统管所有 Python 项目配置。

# 现在的状态(整洁)
myproject/
├─ pyproject.toml    # ✅ 全部配置在一个文件
├─ README.md
├─ LICENSE
├─ src/
│  └─ myproject/
│     ├─ __init__.py
│     └─ ...
└─ tests/
   └─ ...

2. 历史演进:从 setup.py 到 PEP 518/621

里程碑时间内容
setup.py 时代2000s-2016用 Python 代码描述打包配置,灵活但难以静态解析
distutils标准库Python 内置的打包工具,功能有限
setuptools2004+distutils 的超集,至今最流行
PEP 5172017定义构建系统接口标准化
PEP 5182016引入 pyproject.toml,定义构建依赖
PEP 6212021pyproject.toml 中存储项目元数据
PEP 6602021定义 editable 安装标准
现代至今uv, ruff, hatch 等新工具原生支持 pyproject.toml

3. pyproject.toml 完整结构解析

3.1 最小可运行配置

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "myproject"
version = "0.1.0"
description = "A short description"
requires-python = ">=3.9"

上面 8 行代码即可定义一个完整的 Python 包,解释如下:

3.2 [build-system]:构建系统

[build-system]
# 构建前需要安装的工具
requires = ["setuptools>=61.0", "wheel", "setuptools-scm[toml]>=6.2"]

# 构建后端入口点
build-backend = "setuptools.build_meta"
字段说明
requires构建环境先安装这些包(类似 npm 的 devDependencies)
build-backend指定具体调用谁的 build 接口

为什么不能直接用 pip 安装? 因为要先有一个构建系统来从源码生成 .whl 文件,[build-system] 就是声明"我需要这个构建工具"。

3.3 [project]:项目元数据(PEP 621)

[project]
name = "myproject"                    # 必需:包名(PyPI 上的名字)
version = "0.1.0"                     # 版本号(或用 dynamic)
description = "A short description"  # 简短描述
readme = "README.md"                 # 长描述来源
license = {text = "MIT"}             # 许可证
git-keywords = ["python", "tool"]    # PyPI 搜索关键词

# 开发者和维护者
authors = [
    {name = "张三", email = "zhangsan@example.com"},
]
maintainers = [
    {name = "李四", email = "lisi@example.com"},
]

# Python 版本要求
requires-python = ">=3.9"

# 运行依赖(安装时自动装)
dependencies = [
    "requests>=2.28",
    "pydantic>=2.0",
]

# 项目 URL
[project.urls]
Homepage = "https://github.com/you/myproject"
Repository = "https://github.com/you/myproject.git"
Documentation = "https://myproject.readthedocs.io"
"Bug Tracker" = "https://github.com/you/myproject/issues"

# 分类标签(PyPI 显示)
[project.classifiers]
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.9",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
]

3.4 入口点(console_scripts)

[project.scripts]
myproject = "myproject.cli:main"
# 安装后会生成 `myproject` 命令
# 等价于:python -c "from myproject.cli import main; main()"

[project.gui-scripts]
myproject-gui = "myproject.gui:launch"

# 插件系统
[project.entry-points."pytest11"]
myproject = "myproject.pytest_plugin"

3.5 可选依赖(extras)

[project.optional-dependencies]
dev = [           # pip install myproject[dev]
    "pytest>=7.0",
    "pytest-cov",
    "mypy>=1.0",
]
test = [          # pip install myproject[test]
    "pytest>=7.0",
    "pytest-asyncio",
]
docs = [          # pip install myproject[docs]
    "mkdocs",
    "mkdocs-material",
]
all = [           # pip install myproject[all]
    "myproject[dev,test,docs]",
]

4. 实战配置模板

4.1 库项目模板(Library)

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "awesome-lib"
dynamic = ["version"]
description = "一个很棒的 Python 库"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.9"
authors = [
    {name = "张三", email = "zhangsan@example.com"},
]
keywords = ["python", "utility", "async"]
classifiers = [
    "Development Status :: 4 - Beta",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.9",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
]
dependencies = [
    "typing-extensions>=4.0; python_version<'3.10'",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0",
    "pytest-cov",
    "mypy>=1.0",
    "ruff>=0.1.0",
]

[project.urls]
Homepage = "https://github.com/you/awesome-lib"
Repository = "https://github.com/you/awesome-lib.git"
Issues = "https://github.com/you/awesome-lib/issues"

[project.scripts]
awesome = "awesome_lib.cli:main"

[tool.hatch.version]
path = "src/awesome_lib/__init__.py"

[tool.hatch.build.targets.wheel]
packages = ["src/awesome_lib"]

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

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

[tool.ruff]
target-version = "py39"
line-length = 100
select = ["E", "W", "F", "I", "N", "UP", "B"]

4.2 应用程序模板(Application)

[project]
name = "myapp"
version = "0.1.0"
description = "一个 Web 应用"
requires-python = ">=3.11"
dependencies = [
    "fastapi>=0.100",
    "uvicorn[standard]>=0.23",
    "pydantic>=2.0",
    "sqlalchemy>=2.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0",
    "httpx",
    "pytest-asyncio",
]

[tool.ruff]
target-version = "py311"

[tool.pytest.ini_options]
asyncio_mode = "auto"

# 如果需要锁定依赖
# pip install pip-tools
# pip-compile pyproject.toml -o requirements.txt
# pip-compile pyproject.toml --extra dev -o requirements-dev.txt

4.3 现代 uv 项目模板(推荐)

[project]
name = "myproject"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
    "fastapi>=0.111",
    "pydantic>=2.7",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "mypy>=1.10",
    "ruff>=0.4",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

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

[tool.ruff.lint]
select = ["E", "W", "F", "I", "N", "UP", "B", "C4", "SIM"]

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

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

配合 uv 使用:

# 初始化项目
uv init myproject
cd myproject

# 添加依赖
uv add fastapi pydantic
uv add --dev pytest mypy ruff

# 同步并安装
uv sync

# 运行
uv run pytest

5. 构建后端对比:setuptools vs hatch vs flit

维度setuptoolshatchflitpoetry
成熟度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
配置复杂度中等简单极简中等
版本管理手动 / setuptools-scm文件读取 / hatch-vcs手动自动(pyproject)
插件支持丰富丰富
构建速度中等极快中等
pyproject.toml✅ 支持✅ 原生✅ 原生❌ 用自己的格式(till recently)
推荐场景兼容性优先新项目的默认选择纯 Python 库依赖锁定严格

5.1 各后端配置差异

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

[tool.setuptools.packages.find]
where = ["src"]

[tool.setuptools.package-data]
myproject = ["py.typed", "*.json"]
# hatch
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.version]
path = "src/myproject/__init__.py"   # 从代码读取版本

[tool.hatch.build.targets.wheel]
packages = ["src/myproject"]
# flit
[build-system]
requires = ["flit_core>=3.2"]
build-backend = "flit_core.buildapi"

6. 依赖管理:从声明到锁定

6.1 声明依赖(pyproject.toml)

dependencies = [
    "requests>=2.28,<3.0",    # 语义化版本范围
    "pydantic>=2.0",           # 最低版本
    'pywin32>=227; sys_platform == "win32"',   # 平台限定
    "importlib-metadata>=3.6; python_version<'3.10'",  # Python 版本限定
]

6.2 锁定依赖版本

# 方式 1:uv(推荐)
uv run python -c "import fastapi"   # 安装并运行
uv pip compile pyproject.toml -o requirements.txt   # 生成锁定文件

# 方式 2:pip-tools
pip install pip-tools
pip-compile pyproject.toml -o requirements.txt           # 生产依赖
pip-compile pyproject.toml --extra dev -o requirements-dev.txt

# 方式 3:poetry(使用 poetry.lock)
poetry add fastapi
poetry lock

# 安装锁定版本
pip install -r requirements.txt

6.3 uv.lock 格式

uv 生成的 uv.lock 是目前最精确的锁定格式:

version = 1
requires-python = ">=3.11"

[[package]]
name = "fastapi"
version = "0.111.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "pydantic" },
    { name = "starlette" },
    { name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/..." }
wheels = [
    { url = "https://files.pythonhosted.org/...", hash = "sha256:..." },
]

7. 工具配置集中化

pyproject.toml 的一大优势是可以把各种工具的配置都放在一个文件里:

# ========== 测试 ==========
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
asyncio_mode = "auto"
addopts = "-xvs --tb=short"

# ========== 代码风格 ==========
[tool.ruff]
target-version = "py311"
line-length = 100
select = ["E", "W", "F", "I", "N", "UP", "B", "C4"]
ignore = ["E501"]   # 忽略行长度检查(已由 formatter 处理)

[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.ruff.format]
quote-style = "double"
indent-style = "space"

# ========== 类型检查 ==========
[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true
ignore_missing_imports = true

# ========== 覆盖率 ==========
[tool.coverage.run]
source = ["src"]
branch = true

[tool.coverage.report]
exclude_also = [
    "if __name__ == .__main__.:",
    "raise NotImplementedError",
]

# ========== Black(如用 ruff 则不需要) ==========
[tool.black]
line-length = 100
target-version = ["py311"]

# ========== isort(如用 ruff 则不需要) ==========
[tool.isort]
profile = "black"
line_length = 100

8. 从 setup.py 迁移指南

8.1 简单项目迁移

假设你有这个 setup.py

from setuptools import setup, find_packages

setup(
    name="myproject",
    version="0.1.0",
    description="A Python project",
    author="Your Name",
    packages=find_packages(where="src"),
    package_dir={"": "src"},
    install_requires=["requests>=2.28"],
    extras_require={"dev": ["pytest", "mypy"]},
    python_requires=">=3.9",
    entry_points={
        "console_scripts": ["myproject=myproject.cli:main"]
    },
)

迁移为 pyproject.toml

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

[project]
name = "myproject"
version = "0.1.0"
description = "A Python project"
requires-python = ">=3.9"
dependencies = ["requests>=2.28"]

[project.optional-dependencies]
dev = ["pytest", "mypy"]

[project.scripts]
myproject = "myproject.cli:main"

[tool.setuptools.packages.find]
where = ["src"]

8.2 迁移检查清单

  • 删除 setup.py(如有动态逻辑需保留,PEP 621 仍支持 setup.py 辅助)
  • install_requires[project] dependencies
  • extras_require[project.optional-dependencies]
  • entry_points[project.scripts][project.entry-points.*]
  • find_packages[tool.setuptools.packages.find]
  • package_data[tool.setuptools.package-data]
  • 测试:pip install -e . 能正常安装

9. 常见问题与排查

Q1: pip install -e . 失败

# 错误:metadata-generation-failed
# 解决:确保 build-system 配置正确
pip install --upgrade pip setuptools wheel
pip install -e .

Q2: 包找不到(ModuleNotFoundError)

# 确保包路径正确
[tool.setuptools.packages.find]
where = ["src"]           # 告诉 setuptools 去 src/ 下找包
# 或者 hatch:
[tool.hatch.build.targets.wheel]
packages = ["src/myproject"]

Q3: 版本号 dynamic

# 方式 1:从文件读取(hatch)
[project]
dynamic = ["version"]

[tool.hatch.version]
path = "src/myproject/__init__.py"
# __init__.py 中:__version__ = "0.1.0"

# 方式 2:从 git tag 读取(setuptools-scm)
[build-system]
requires = ["setuptools>=61.0", "setuptools-scm[toml]>=6.2"]

[project]
dynamic = ["version"]

[tool.setuptools_scm]
write_to = "src/myproject/_version.py"

Q4: 想在 pyproject.toml 中执行自定义代码

PEP 621 是声明式的,不能写 Python 代码。如需动态逻辑:

  1. 保留 setup.py 作为 pyproject.toml 的补充(build 系统仍会读取)
  2. 使用 hatch 的构建钩子
  3. setuptools-scm 等工具处理动态版本

延伸阅读


pyproject.toml 是 Python 工程化的基础设施。掌握它,你就掌握了现代 Python 项目的"总控室"。建议每个新项目都从它开始,每个旧项目都向它迁移。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「python」更多文章

  1. Python 高级异步编程:Trio 结构化并发与 AnyIO 兼容层
  2. Python 数据工程与 ETL 管道实战
  3. Python 元编程与动态特性深度解析