pyproject.toml是 Python 项目的"身份证"——它告诉构建工具如何打包你的代码,告诉编辑器如何格式化它,告诉 CI 如何测试它。本文从标准演进历史到逐字段解析,让你彻底掌握这份配置文件。
目录
- 为什么是 pyproject.toml
- 历史演进:从 setup.py 到 PEP 518/621
- pyproject.toml 完整结构解析
- 实战配置模板
- 构建后端对比:setuptools vs hatch vs flit
- 依赖管理:从声明到锁定
- 工具配置集中化
- 从 setup.py 迁移指南
- 常见问题与排查
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 内置的打包工具,功能有限 |
setuptools | 2004+ | distutils 的超集,至今最流行 |
| PEP 517 | 2017 | 定义构建系统接口标准化 |
| PEP 518 | 2016 | 引入 pyproject.toml,定义构建依赖 |
| PEP 621 | 2021 | 在 pyproject.toml 中存储项目元数据 |
| PEP 660 | 2021 | 定义 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
| 维度 | setuptools | hatch | flit | poetry |
|---|---|---|---|---|
| 成熟度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 配置复杂度 | 中等 | 简单 | 极简 | 中等 |
| 版本管理 | 手动 / 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 代码。如需动态逻辑:
- 保留
setup.py作为pyproject.toml的补充(build 系统仍会读取) - 使用
hatch的构建钩子 - 用
setuptools-scm等工具处理动态版本
延伸阅读
- Python 现代工具链:uv + ruff + mypy 全链路 —— 与 pyproject.toml 配套的现代开发工具
- Python 部署与分发指南 —— 打包后的发布与部署
- Python 测试与质量工程 —— pytest 在 pyproject.toml 中的配置
- Vercel 部署 Python 项目 —— 服务器端 Python 部署
pyproject.toml 是 Python 工程化的基础设施。掌握它,你就掌握了现代 Python 项目的"总控室"。建议每个新项目都从它开始,每个旧项目都向它迁移。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。