构建并发布Python包到PyPI完整指南

本文将详细介绍如何构建并发布一个 Python 包到 Python Package Index (PyPI,可以理解为 Python 的”应用商店”,所有人都可以上传和下载 Python 包),涵盖从手动发布到 CI 自动化发布的完整流程。

📑 目录

  1. 项目结构规划
  2. 编写 pyproject.toml
  3. 安装构建工具
  4. 本地构建 Python 包
  5. 手动发布到 PyPI
  6. 测试安装
  7. GitHub Actions 自动发布
  8. PyPI Trusted Publisher 配置
  9. 进阶:使用 hatch-vcs 自动管理版本
  10. 完整流程总结

一、项目结构规划

推荐使用 src/ 布局,这是现代 Python 项目的最佳实践:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
my-package/

├── src/
│ └── my_package/
│ ├── __init__.py
│ └── hello.py

├── tests/
│ └── test_hello.py

├── .github/
│ └── workflows/
│ └── publish.yml

├── pyproject.toml
├── README.md
└── LICENSE

示例代码 src/my_package/hello.py

1
2
def say_hello():
print("Hello from my package!")

src/my_package/__init__.py

1
2
3
from my_package.hello import say_hello

__all__ = ["say_hello"]

为什么使用 src/ 布局?

  • 避免本地模块与安装后的模块混淆
  • 强制在测试时使用已安装的包
  • 防止意外导入未打包的模块

二、编写 pyproject.toml

pyproject.toml 是 Python 包的核心配置文件(可以把它想象成项目的”身份证”,记录了包名、版本、依赖等所有元信息),遵循 PEP 517/518/621 规范。

2.1 基础配置(使用 uv 构建)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[build-system]
requires = ["uv"]
build-backend = "uv.build"

[project]
name = "my-package-demo"
version = "0.1.0"
description = "A demo Python package"
readme = "README.md"
requires-python = ">=3.9"
license = "MIT"
authors = [
{ name = "Your Name", email = "you@example.com" }
]

dependencies = []

[project.urls]
Homepage = "https://github.com/username/my-package"
Repository = "https://github.com/username/my-package"

2.2 进阶配置(使用 hatchling + hatch-vcs)

使用 hatchling + hatch-vcs 实现更强大的构建和版本管理:

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
[build-system]
requires = ["hatch-vcs", "hatchling"]
build-backend = "hatchling.build"

[project]
name = "my-package"
description = "A demo Python package with advanced features"
readme = "README.md"
requires-python = ">=3.12"
license = "MIT"
authors = [{ name = "Your Name", email = "you@example.com" }]
dependencies = [
"pydantic>=2.0",
"typer>=0.15.0",
]
dynamic = ["version"] # 版本号从 git tag 自动获取

# CLI 入口点
[project.scripts]
my-cli = "my_package.cli:app"

# 插件入口点(可选)
[project.entry-points."my_package.plugins"]
default = "my_package.plugins.default:DefaultPlugin"

[project.urls]
Homepage = "https://github.com/username/my-package"
Repository = "https://github.com/username/my-package"

# hatch 构建配置
[tool.hatch.build.targets.wheel]
packages = ["src/my_package"]

# 从 git tag 获取版本
[tool.hatch.version]
source = "vcs"

[tool.hatch.build.hooks.vcs]
version-file = "src/my_package/_version.py"

2.3 关键字段说明

字段 作用
name PyPI 包名(用户 pip install 时使用)
version 版本号(或设为 dynamic 自动生成)
dependencies 运行时依赖
requires-python 支持的 Python 版本
project.scripts 安装后可用的命令行工具
project.entry-points 插件系统入口点

三、安装构建工具

3.1 安装 uv(推荐)

Linux / macOS:

1
curl -LsSf https://astral.sh/uv/install.sh | sh

Windows:

1
irm https://astral.sh/uv/install.ps1 | iex

验证安装:

1
uv --version

3.2 安装 hatch(可选)

如果使用 hatchling 构建:

1
pip install hatch

四、本地构建 Python 包

进入项目目录执行构建:

1
2
cd my-package
uv build

构建成功后会生成 dist/ 目录:

1
2
3
dist/
├── my_package_demo-0.1.0.tar.gz # 源码包
└── my_package_demo-0.1.0-py3-none-any.whl # wheel 二进制包
文件类型 说明
.tar.gz 源码分发包(sdist),安装时需要构建
.whl wheel 包——Python 包的”安装包”格式,已经预编译好,安装时直接解压就行,比 sdist(源码包)快得多

五、手动发布到 PyPI

5.1 安装上传工具

1
pip install twine

5.2 上传到 PyPI

1
twine upload dist/*

系统会提示输入凭据:

1
2
username: __token__
password: pypi-xxxx # 在 PyPI 账号中生成的 API Token

5.3 获取 PyPI Token

  1. 登录 https://pypi.org
  2. 进入 Account Settings → API tokens
  3. 创建新 Token,选择作用域(整个账号或特定项目)
  4. 复制 Token(以 pypi- 开头)

发布成功后可在以下地址查看:

1
https://pypi.org/project/my-package-demo/

六、测试安装

从 PyPI 安装并测试:

1
pip install my-package-demo

验证:

1
2
3
from my_package.hello import say_hello
say_hello()
# 输出: Hello from my package!

七、GitHub Actions 自动发布

手动发布容易出错,生产环境推荐使用 CI 自动化。以下是推荐的配置:

7.1 创建 workflow 文件

.github/workflows/publish.yml

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
name: "CI - Publish"

on:
push:
tags: ["v*"] # 仅在推送 v 开头的 tag 时触发
workflow_dispatch: # 允许手动触发

permissions:
contents: read
id-token: write # OIDC 认证必需

jobs:
build:
name: Build Distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.13"

- name: Build package
run: uv build

- name: Upload distribution
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/

publish:
name: Publish To PyPI
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/my-package
steps:
- name: Download distribution
uses: actions/download-artifact@v4
with:
name: dist
path: dist/

- name: Publish package
uses: pypa/gh-action-pypi-publish@release/v1

7.2 workflow 结构说明

1
2
3
4
5
6
7
8
9
10
11
触发条件

build job(构建)
├── checkout 代码
├── 安装 uv
├── uv build 构建包
└── 上传 artifact

publish job(发布)
├── 下载 artifact
└── 发布到 PyPI

关键设计:

  1. build 与 publish 分离 - 构建产物通过 artifact 传递,职责清晰
  2. tag 触发 - 只有推送版本 tag 才触发发布
  3. OIDC 认证 - 无需存储 PyPI Token

八、PyPI Trusted Publisher 配置

PyPI Trusted Publisher 使用 OIDC(OpenID Connect)认证,无需在 GitHub 存储 API Token,更安全。

8.1 配置步骤

  1. 登录 PyPI:https://pypi.org/manage/account/publishing/

  2. 点击 “Add a new pending publisher”(新项目)或在项目设置中添加

  3. 填写信息:

    • Owner: GitHub 用户名或组织名
    • Repository name: 仓库名
    • Workflow name: publish.yml(与你的 workflow 文件名一致)
    • Environment name: pypi(与 workflow 中的 environment 一致)
  4. 保存配置

8.2 确保 workflow 配置正确

1
2
3
4
5
6
7
permissions:
id-token: write # 必须有这个权限

jobs:
publish:
environment:
name: pypi # 必须与 PyPI 配置的 environment 一致

九、进阶:使用 hatch-vcs 自动管理版本

手动维护版本号容易遗忘或出错。推荐使用 hatch-vcs 从 git tag 自动获取版本。

9.1 配置 pyproject.toml

1
2
3
4
5
6
7
8
9
10
11
12
13
[build-system]
requires = ["hatch-vcs", "hatchling"]
build-backend = "hatchling.build"

[project]
name = "my-package"
dynamic = ["version"] # 版本号动态生成

[tool.hatch.version]
source = "vcs" # 从版本控制系统获取

[tool.hatch.build.hooks.vcs]
version-file = "src/my_package/_version.py" # 生成版本文件

9.2 工作流程

1
2
3
4
5
6
7
git tag v1.2.3

构建时自动读取 tag

版本号设为 1.2.3

生成 _version.py 文件

优点:

  • 版本号与 git tag 强绑定,不会出错
  • 无需手动修改代码中的版本号
  • 代码中可通过 _version.py 访问版本

十、完整流程总结

10.1 手动发布流程

1
2
3
4
5
6
7
8
9
10
11
创建项目结构(src/ 布局)

编写 pyproject.toml

uv build(构建)

生成 dist/(.tar.gz + .whl)

twine upload dist/*

发布到 PyPI

10.2 自动发布流程

1
2
3
4
5
6
7
8
9
10
11
12
13
Developer

git tag v1.0.0 && git push origin v1.0.0

GitHub Actions 触发

uv build 构建

OIDC 认证

自动发布到 PyPI

用户: pip install my-package

10.3 推荐实践清单

实践 说明
使用 src/ 布局 避免导入混淆
使用 tag 触发发布 版本控制清晰
build 与 publish 分离 职责明确,易于调试
使用 Trusted Publisher 无需存储 Token,更安全
使用 hatch-vcs 自动管理版本号
添加测试 确保发布前质量

📋 附录:常用命令速查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 安装 uv
curl -LsSf https://astral.sh/uv/install.sh | sh

# 构建包
uv build

# 手动上传
twine upload dist/*

# 创建并推送 tag
git tag v1.0.0
git push origin v1.0.0

# 本地测试安装
pip install dist/*.whl

# 从 PyPI 安装
pip install my-package-demo

🎯 自我检验清单

读完本文后,你应该能做到以下几点:

  • 能独立创建一个符合 src/ 布局规范的 Python 项目结构
  • 能编写 pyproject.toml,正确配置包名、版本、依赖、入口点等关键字段
  • 能使用 uv build 在本地构建出 .tar.gz.whl 两种分发包
  • 能使用 twine 手动将包上传到 PyPI
  • 能编写 GitHub Actions workflow,实现 tag 触发自动构建与发布
  • 能在 PyPI 上配置 Trusted Publisher,用 OIDC 替代 API Token 认证
  • 能使用 hatch-vcs 从 git tag 自动生成版本号,不再手动维护版本
  • 能区分 sdist 与 wheel 两种分发格式的适用场景

📚 参考资源