发布包到公共市场
把可安装库发到 PyPI(Python Package Index),别人就能 pip install / uv add。本篇在「项目与包管理」之上,走通 构建 → 试发 → 正式发布;工具链与绿场推荐一致,默认用 uv。
先说结论:公共市场上传的是 发行物(sdist / wheel),不是整份源码仓库。包名在 PyPI 上全局唯一,同一版本一旦发布不可覆盖——错了只能发新版本。
发之前要齐什么
| 项 | 说明 |
|---|---|
| 可安装布局 | 常见 src/包名/ 或扁平包目录;见「模块与包」 |
pyproject.toml | [project] 元数据 + [build-system] 构建后端 |
| README / license | 通常 readme = "README.md";许可证写进元数据 |
| 账号 | pypi.org 与建议先用的 test.pypi.org 各注册一份 |
uv init --package mylib 会生成带 [build-system] 的库项目骨架;已有应用项目若要发布,需补上构建配置(见下)。
最小可发布配置
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "example-pkg" # PyPI 上的包名(可含连字符)
version = "0.1.0"
description = "一句话说明"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
authors = [{ name = "你的名字", email = "you@example.com" }]
dependencies = [
"requests>=2.31",
]
[project.urls]
Homepage = "https://github.com/you/example-pkg"
# 可选:命令行入口
# [project.scripts]
# example-cli = "example_pkg.cli:main"
注意:
- import 名 与 PyPI 名 可以不同(如包名
example-pkg,代码里import example_pkg) - 没有
[build-system]时,uv build会回退到旧式 setuptools;正式发布请显式声明(常用 hatchling) - 若项目用了
tool.uv.sources(本地路径 / git 源),发布构建建议:uv build --no-sources,避免别人装不上
构建发行物
uv build
# 或:uv build --no-sources
产物默认在 dist/:
dist/
example_pkg-0.1.0.tar.gz # sdist
example_pkg-0.1.0-py3-none-any.whl # wheel
本地先装进干净环境试一把:
uv run --with ./dist/example_pkg-0.1.0-py3-none-any.whl --no-project -- \
python -c "import example_pkg; print(example_pkg.__file__)"
改版本
uv version # 查看
uv version 0.2.0 # 设为指定版本
uv version --bump patch # 0.1.0 → 0.1.1
uv version --bump minor # 0.1.1 → 0.2.0
--bump 还支持 major / alpha / beta / rc / stable 等。发正式版前确认 pyproject.toml 里的版本与 changelog / git tag 一致。
先发 TestPyPI(强烈建议)
TestPyPI 是练习场:流程与正式站相同,不影响真实索引。
- 在 test.pypi.org 注册,Account settings → API tokens 生成 token
- 配置命名索引(可选,便于复用):
[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true
- 上传:
uv publish --index testpypi --token pypi-...
# 或一次性:
uv publish --publish-url https://test.pypi.org/legacy/ --token pypi-...
- 从 TestPyPI 安装验证:
uv run --with example-pkg --index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ --no-project -- \
python -c "import example_pkg"
依赖若只在正式 PyPI 上,常需要 --extra-index-url 指向正式源。
正式发布到 PyPI
- 在 pypi.org 创建项目(或首次上传时自动创建)
- 生成 API token(Scoped 到该项目更稳妥)
- 构建并上传:
uv build --no-sources
uv publish --token pypi-...
# 也可用环境变量:UV_PUBLISH_TOKEN
PyPI 已不支持 账号密码上传;token 等价于用户名 __token__ + 密码为 token。
装回验证:
uv run --with example-pkg --no-project --refresh-package example-pkg -- \
python -c "import example_pkg"
CI:Trusted Publishing(推荐)
在 GitHub Actions 等环境发版时,优先用 PyPI 的 Trusted Publisher(OIDC),不必把长期 token 放进 Secrets:
- 在 PyPI 项目设置里添加 Trusted Publisher(绑定仓库、workflow、环境)
- workflow 给予
id-token: write,检出代码后执行uv build+uv publish - 无需
--token;uv 会使用短期身份令牌
细节以 PyPI Trusted Publishers 与 uv 打包指南 为准。
发布检查表
- 包名在 PyPI / TestPyPI 上可注册且不冲突
version未在目标索引上用过(同版本不可覆盖)README、依赖、requires-python在构建后的元数据里正确uv build --no-sources成功;wheel 在干净环境可 import- 先 TestPyPI,再正式 PyPI
- token / Trusted Publisher 权限最小化;token 不进仓库
- 打 git tag(如
v0.1.0)与发布版本对齐,便于追溯
常见坑
| 现象 | 常见原因 |
|---|---|
| 上传报版本已存在 | 该版本曾发过;只能升版本 |
| 别人装上后 import 失败 | 包布局 / hatch 包路径未配好;先本地装 wheel 验证 |
| 构建依赖解析异常 | 发布时未加 --no-sources,带上了仅本地可用的源 |
| TestPyPI 装依赖失败 | 传递依赖不在 TestPyPI;加正式源作 extra index |
| 中途失败部分文件已上传 | PyPI 上可重跑相同命令;相同文件会被忽略 |
私有源(公司 Artifactory 等)同样用 [[tool.uv.index]] 配 publish-url,再 uv publish --index <name>。本篇只覆盖公共市场;私有策略跟团队规范。
要点
- 公共市场 = PyPI;先 TestPyPI 再正式站
- 声明用
pyproject.toml,构建用uv build,上传用uv publish - 版本不可覆盖;发错就升版本
- 鉴权用 API token 或 CI Trusted Publishing,不要用账号密码
- 依赖与环境管理仍见「项目与包管理」