前置知识: Python

打包与发布

2 minAdvanced2026/6/14

Python打包与发布详解:setuptools、pyproject.toml。

概述

Python 的打包与发布是将项目代码转化为可安装、可分发包的过程。现代 Python 打包以 pyproject.toml 为核心配置文件,使用 setuptools、hatchling 或 flit 等构建后端。发布到 PyPI 后,其他开发者可以通过 pip 安装使用。

基础概念

包的

  • Source Distribution (sdist):源码包,包含项目源代码
  • Wheel (bdist_wheel):预编译包,安装更快,不包含 .pyc 文件
  • 纯 Python Wheel:不含 C 扩展,跨平台
  • 平台相关 Wheel:含 C 扩展,需针对不同平台构建

项目结构

myproject/
├── pyproject.toml       # 项目配置(核心)
├── src/
│   └── mypackage/
│       ├── __init__.py
│       └── core.py
├── tests/
│   └── test_core.py
├── README.md
└── LICENSE

推荐使用 src 布局(源代码在 src 目录下),可以避免安装前意外导入未打包的代码。

快速上手

创建 pyproject.toml

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

[project]
name = "mypackage"
version = "1.0.0"
description = "一个示例 Python 包"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.9"
authors = [
    {name = "作者名", email = "author@example.com"},
]
dependencies = [
    "requests>=2.28.0",
    "click>=8.0",
]

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

[project.scripts]
mycli = "mypackage.cli:main"

[project.urls]
Homepage = "https://github.com/user/mypackage"

构建与安装

# 安装构建工具
pip install build

# 构建包
python -m build

# 生成 dist/mypackage-1.0.0.tar.gz 和 dist/mypackage-1.0.0-py3-none-any.whl

# 本地安装测试
pip install dist/mypackage-1.0.0-py3-none-any.whl

# 开发模式安装(可编辑安装)
pip install -e .

详细用法

版本管理

# 方式一:直接在 pyproject.toml 中指定
version = "1.2.3"

# 方式二:动态从包中读取版本
[project]
dynamic = ["version"]

[tool.setuptools.dynamic]
version = {attr = "mypackage.__version__"}

在包中定义版本:

# mypackage/__init__.py
__version__ = "1.2.3"

依赖管理

[project]
dependencies = [
    # 必须依赖
    "requests>=2.28.0,<3.0",
    "click>=8.0",
    "pydantic>=2.0",
]

[project.optional-dependencies]
# 开发依赖
dev = [
    "pytest>=7.0",
    "mypy>=1.0",
    "ruff>=0.1.0",
]
# 文档构建依赖
docs = [
    "sphinx>=5.0",
    "sphinx-rtd-theme",
]
# 特定功能依赖
redis = ["redis>=4.0"]
mysql = ["pymysql>=1.0"]
# 全部可选依赖
all = ["mypackage[redis,mysql]"]

安装可选依赖:

pip install mypackage[dev]
pip install mypackage[redis,mysql]
pip install mypackage[all]

入口点

# 命令行工具
[project.scripts]
mycli = "mypackage.cli:main"

# 插件注册
[project.entry-points."mypackage.plugins"]
redis = "mypackage_redis:RedisPlugin"
mysql = "mypackage_mysql:MySQLPlugin"

包含与排除文件

[tool.setuptools.packages.find]
where = ["src"]
include = ["mypackage*"]
exclude = ["tests*"]

[tool.setuptools.package-data]
mypackage = ["py.typed", "data/*.json"]

数据文件

# 在包中包含数据文件
from importlib.resources import files

# 读取包内数据文件
data_path = files("mypackage").joinpath("data/config.json")
content = data_path.read_text(encoding="utf-8")

常见场景

场景一:发布到 PyPI

# 1. 注册 PyPI 账号
# https://pypi.org/account/register/

# 2. 安装发布工具
pip install twine

# 3. 构建包
python -m build

# 4. 检查包
twine check dist/*

# 5. 上传到 TestPyPI(先测试)
twine upload --repository testpypi dist/*

# 6. 上传到正式 PyPI
twine upload dist/*

场景二:使用 Trusted Publisher

# GitHub Actions 自动发布
# 在 PyPI 上配置 Trusted Publisher

# .github/workflows/publish.yml
# name: Publish
# on:
#   release:
#     types: [published]
# jobs:
#   publish:
#     runs-on: ubuntu-latest
#     permissions:
#       id-token: write
#     steps:
#       - uses: actions/checkout@v4
#       - uses: actions/setup-python@v5
#       - run: pip install build
#       - run: python -m build
#       - uses: pypa/gh-action-pypi-publish@release/v1

场景三:C 扩展打包

[build-system]
requires = ["setuptools>=68.0", "wheel", "Cython"]
build-backend = "setuptools.build_meta"
# setup.py(C 扩展需要)
from setuptools import setup, Extension
from Cython.Build import cythonize

extensions = [
    Extension("mypackage._fast", ["src/mypackage/_fast.pyx"]),
]

setup(
    ext_modules=cythonize(extensions),
)

注意事项

  • 使用 src 布局避免安装前意外导入未打包代码
  • 版本号遵循 PEP 440 语义化版本规范
  • 发布前务必在 TestPyPI 上测试
  • pyproject.toml 中的 license 字段推荐使用 SPDX 标识符
  • 构建时确保不包含敏感信息(.env、密钥等)
  • 使用 pip install -e . 进行开发模式安装,修改代码后无需重新安装

进阶用法

使用 hatchling 构建后端

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

[project]
name = "mypackage"
version = "1.0.0"

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

[tool.hatch.envs.default]
dependencies = ["pytest", "mypy"]

[tool.hatch.envs.default.scripts]
test = "pytest {args:tests}"
typecheck = "mypy src"

动态版本号

# 使用 setuptools_scm 从 git 标签自动生成版本号
[build-system]
requires = ["setuptools>=68.0", "setuptools_scm>=8.0"]
build-backend = "setuptools.build_meta"

[project]
dynamic = ["version"]

[tool.setuptools_scm]
# 从 git tag 自动推断版本号

条件依赖

[project]
dependencies = [
    "tomli>=2.0; python_version < '3.11'",
    "importlib-metadata>=6.0; python_version < '3.10'",
    "typing-extensions>=4.0; python_version < '3.11'",
]

py.typed 标记

# 在包中包含 py.typed 文件,表示支持类型检查
[tool.setuptools.package-data]
mypackage = ["py.typed"]
src/mypackage/
├── __init__.py
├── py.typed       # 空文件,标记支持类型检查
└── core.pyi       # 类型存根文件(可选)