前言:为什么 PyPA 要自己造一个构建后端?
Python 打包世界很长一段时间只有 setuptools。它功能强大,但问题也明显:
setup.py 本质是可执行代码,有安全风险
- 配置分散在
setup.py + setup.cfg + MANIFEST.in 中
- 构建速度慢(每次都要执行 Python 代码来解析配置)
PyPA 的目标是:一个纯声明式的、PEP 621 原生的、快速且可扩展的构建后端。这就是 hatchling。
hatchling 的定位:setuptools 的现代化替代品。不是”又一个新的打包工具”,而是”PyPA 认为 Python 打包该有的样子”。
它与 Hatch 的关系:Hatch 是项目管理器(类似 Poetry),hatchling 是其中的构建引擎。但你完全不需要安装 Hatch,hatchling 可以作为独立构建后端使用。
一、快速开始:3 行配置跑起来
1.1 最小 pyproject.toml
1 2 3 4 5 6 7
| [build-system] requires = ["hatchling"] build-backend = "hatchling.build"
[project] name = "my-package" version = "0.1.0"
|
1 2 3 4
| pip install build python -m build
|
就这三行——不需要 setup.py,不需要 setup.cfg,不需要 MANIFEST.in。hatchling 自动发现 src/ 或根目录下的包。
1.2 带依赖和入口点的完整配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| [build-system] requires = ["hatchling"] build-backend = "hatchling.build"
[project] name = "my-cli" version = "0.1.0" description = "A hatchling-powered CLI" readme = "README.md" requires-python = ">=3.10" license = {text = "MIT"} authors = [{name = "You", email = "you@example.com"}] dependencies = [ "click>=8.1", "rich>=13.0", ]
[project.optional-dependencies] dev = ["pytest>=8.0", "ruff>=0.8"]
[project.scripts] mycli = "my_cli.main:cli"
[project.urls] Repository = "https://github.com/you/my-cli"
|
1 2
| pip install . mycli --help
|
二、版本管理:hatchling 最亮眼的内置功能
setuptools 需要额外安装 setuptools-scm 来管理版本,hatchling 内置了多种版本方案。
2.1 静态版本(最简单)
1 2
| [project] version = "1.0.0"
|
2.2 从文件读取(推荐)
1 2 3 4 5
| [project] dynamic = ["version"]
[tool.hatch.version] path = "src/my_package/__init__.py"
|
2.3 正则提取(灵活)
1 2 3
| [tool.hatch.version] path = "src/my_package/__init__.py" pattern = "VERSION = '(?P<version>[^']+)'"
|
2.4 从 Git tag 自动推断
1 2
| [tool.hatch.version] source = "vcs"
|
1 2 3 4 5 6 7 8
| git tag v1.0.0 python -m build
git commit -m "wip" python -m build
|
2.5 版本方案对比
| 方案 |
配置 |
适用场景 |
| 静态 |
version = "1.0.0" |
简单项目 |
| 文件读取 |
path = "xxx/__init__.py" |
大多数项目 |
| 正则提取 |
path + pattern |
非标准格式 |
| Git tag |
source = "vcs" |
CI/CD 自动化发布 |
| 自定义 |
source = "code" + 回调 |
复杂场景 |
三、包发现:零配置的艺术
3.1 默认行为
hatchling 默认自动发现,你什么都不用配:
1 2 3 4 5
| my-project/ ├── pyproject.toml └── src/ └── my_package/ └── __init__.py
|
3.2 手动指定(src-layout)
1 2
| [tool.hatch.build.targets.wheel] packages = ["src/my_package"]
|
1 2 3 4 5 6
| [tool.hatch.build.targets.wheel] packages = [ "src/my_core", "src/my_plugins", ]
|
3.3 强制只打包纯 Python 包
1 2
| [tool.hatch.build.targets.wheel] only-packages = true
|
3.4 排除文件
1 2 3 4 5
| [tool.hatch.build.targets.wheel] exclude = ["src/my_package/tests", "src/my_package/_dev"]
[tool.hatch.build.targets.sdist] exclude = ["tests", "docs", ".github"]
|
四、构建目标:wheel 和 sdist 的精细控制
4.1 独立配置两种目标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| [tool.hatch.build.targets.wheel] packages = ["src/my_package"] only-packages = true
[tool.hatch.build.targets.sdist] include = [ "/tests", "/docs", "/.github", ] exclude = [ "*.pyc", "__pycache__", ]
|
4.2 数据文件自动包含
hatchling 默认使用 VCS(git)来确定哪些文件属于项目,会自动包含被 git 跟踪的所有文件。这也意味着:
1 2 3 4 5 6 7 8 9 10
| git status
[tool.hatch.build.targets.sdist] include = ["/data/important.csv"]
[tool.hatch.build.targets.sdist] exclude = ["/tests/fixtures/large_files"]
|
4.3 构建时强制版本检查
1 2 3
| [tool.hatch.build] require-runtime-dependencies = true skip-excluded-dirs = true
|
五、构建钩子(Build Hooks):在构建流程中注入自定义逻辑
这是 hatchling 的高级特性,允许你在构建的特定阶段执行自定义代码。
5.1 钩子类型
| 钩子 |
触发时机 |
用途 |
version |
读取版本时 |
自定义版本获取逻辑 |
initialize |
构建开始前 |
准备工作(生成文件、下载资源) |
metadata |
元数据收集后 |
动态修改项目元数据 |
finalize |
构建完成后 |
后处理(压缩、校验) |
5.2 实战:构建前自动生成版本文件
1 2
| [tool.hatch.build.hooks.custom] path = "hatch_build.py"
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import datetime from hatchling.builders.hooks.plugin.interface import BuildHookInterface
class CustomHook(BuildHookInterface): def initialize(self, version, build_data): """构建前:生成 _version.py""" output_path = self.root / "src" / "my_package" / "_version.py" today = datetime.date.today().isoformat() output_path.write_text( f'__version__ = "{version}"\n' f'__build_date__ = "{today}"\n' )
def finalize(self, version, build_data, artifact_path): """构建后:打印构建信息""" print(f"Built {artifact_path} (version {version})")
|
5.3 使用第三方钩子
1 2 3 4
| [build-system] requires = ["hatchling", "hatch-odoo"]
[tool.hatch.build.hooks.odoo-addons]
|
安装 hatch-odoo 后,它自动注册了 odoo-addons 钩子,会在构建时处理 Odoo 模块的特殊需求。
六、插件系统
hatchling 的插件架构非常灵活:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 构建钩子(Build Hooks) ├── CustomHook # 自定义 Python 脚本 ├── hatch-vcs # 增强 VCS 集成 ├── hatch-odoo # Odoo 模块支持 └── hatch-mypyc # mypyc 编译支持
版本源(Version Sources) ├── static # 内置:静态版本 ├── code # 内置:从代码文件读取 ├── regex # 内置:正则提取 ├── vcs # 内置:从 Git tag 读取 └── 自定义 # 实现 VersionSourceInterface
元数据钩子(Metadata Hooks) └── 自定义 # 实现 MetadataHookInterface
|
七、高级配置
7.1 共享库 / 数据目录
1 2
| [tool.hatch.build.targets.wheel.shared-data] "extra-scripts/my-helper" = "share/my_package/helper"
|
构建后,extra-scripts/my-helper 会被复制到 wheel 的 share/my_package/helper 位置。
7.2 强制包含特定文件到 wheel
1 2 3
| [tool.hatch.build.targets.wheel.force-include] "src/my_package/py.typed" = "my_package/py.typed" "README.md" = "my_package/README.md"
|
7.3 环境标记
1 2 3 4 5 6 7 8
| [tool.hatch.build.targets.wheel] bypass-selection = true
[[tool.hatch.build.targets.wheel.force-include]] path = "src/my_package/_win32.pyd" dest = "my_package/_win32.pyd" condition = "sys_platform == 'win32'"
|
7.4 严格模式
1 2
| [tool.hatch.build] strict-naming = true
|
八、实战案例:一个完整的 CLI 项目
8.1 项目结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| weather-cli/ ├── pyproject.toml ├── README.md ├── LICENSE ├── src/ │ └── weather_cli/ │ ├── __init__.py │ ├── cli.py │ ├── api.py │ ├── formatters.py │ ├── py.typed │ └── templates/ │ ├── report.html │ └── summary.txt └── tests/ ├── __init__.py ├── test_cli.py └── test_api.py
|
8.2 pyproject.toml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| [build-system] requires = ["hatchling"] build-backend = "hatchling.build"
[project] name = "weather-cli" description = "A command-line weather tool built with hatchling" readme = "README.md" license = {text = "MIT"} requires-python = ">=3.10" authors = [{name = "You", email = "you@example.com"}] dynamic = ["version"]
dependencies = [ "requests>=2.28,<3.0", "click>=8.1,<9.0", "rich>=13.0", ]
[project.optional-dependencies] dev = [ "pytest>=8.0", "pytest-cov>=6.0", "ruff>=0.8", "mypy>=1.14", ]
[project.scripts] weather = "weather_cli.cli:main"
[project.urls] Repository = "https://github.com/you/weather-cli" Documentation = "https://weather-cli.readthedocs.io" Issues = "https://github.com/you/weather-cli/issues"
[tool.hatch.version] path = "src/weather_cli/__init__.py"
[tool.hatch.build.targets.wheel] packages = ["src/weather_cli"]
[tool.hatch.build.targets.wheel.force-include] "src/weather_cli/py.typed" = "weather_cli/py.typed"
[tool.hatch.build.targets.sdist] exclude = ["tests", ".github"]
[tool.ruff] line-length = 100 target-version = "py310"
[tool.mypy] python_version = "3.10" strict = true
[tool.pytest.ini_options] testpaths = ["tests"]
|
8.3 构建与发布
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| pip install -e ".[dev]"
pytest tests/ -v
python -m build
twine check dist/*
twine upload dist/*
|
9.1 对照表
| setuptools |
hatchling |
find_packages(where="src") |
packages = ["src/my_package"] |
package_dir={"": "src"} |
自动处理,无需声明 |
setuptools-scm |
[tool.hatch.version] source = "vcs" (内置) |
include_package_data=True |
VCS 自动跟踪(默认) |
MANIFEST.in |
不再需要(VCS 自动发现) |
ext_modules=[Extension(...)] |
❌ 不支持,请留在 setuptools |
entry_points={"console_scripts": ...} |
[project.scripts](PEP 621) |
9.2 迁移步骤
1 2 3 4 5 6 7 8
|
python -m build pip install dist/*.whl
|
⚠️ C 扩展项目不能迁移。hatchling 不处理 C 编译,这种情况必须留在 setuptools。
十、与其他构建后端对比
| 特性 |
hatchling |
setuptools |
flit |
PDM backend |
| PEP 621 原生 |
✅ |
⚠️ |
✅ |
✅ |
| 构建速度 |
✅ 快 |
⚠️ 慢 |
✅ 最快 |
✅ 快 |
| 版本管理 |
✅ 内置 4 种 |
⚠️ 需 setuptools-scm |
✅ 内置 |
✅ 内置 |
| 构建钩子 |
✅ 一流 |
✅ cmdclass |
❌ |
⚠️ |
| VCS 集成 |
✅ 默认 |
⚠️ MANIFEST.in |
✅ |
✅ |
| C 扩展 |
❌ |
✅ |
❌ |
⚠️ |
| 配置复杂度 |
✅ 低 |
⚠️ 中 |
✅ 最低 |
⚠️ 中 |
| 插件生态 |
✅ 增长中 |
✅ 最丰富 |
❌ |
✅ |
十一、常见问题
11.1 “hatchling 找不到我的包”
1 2 3 4 5 6 7
|
git ls-files src/
[tool.hatch.build.targets.wheel] packages = ["src/my_package"]
|
11.2 “构建后 wheel 是空的”
1 2 3
| [tool.hatch.build.targets.wheel] packages = ["src/my_package"]
|
11.3 “数据文件没被包含”
1 2 3 4
| git add src/my_package/templates/ git commit -m "add templates" python -m build
|
11.4 “动态版本不生效”
1 2 3 4 5 6 7
| [project] dynamic = ["version"]
[tool.hatch.version] path = "src/my_package/__init__.py"
|
总结
hatchling 的核心优势
- 零配置: 开箱即用,VCS 自动发现文件
- 内置版本管理: 静态 / 文件 / 正则 / Git tag,四种方案无需额外插件
- 构建钩子: 在构建流程任意阶段注入自定义逻辑
- PEP 621 原生: 从一开始就遵循 PyPA 标准
- 快速: 纯声明式解析,比 setuptools 快数倍
什么时候用 hatchling?
| 场景 |
建议 |
| 纯 Python 新项目 |
✅ 首选用 hatchling |
| 需要 C 扩展 |
❌ 必须用 setuptools |
| 存量 setuptools 项目 |
⚠️ 看情况,纯 Python 可迁 |
| 需要自定义构建逻辑 |
✅ 用 hatchling 的 build hooks |
📋 参考资料: