不管你是写爬虫脚本、量化策略,还是做数据可视化工具,最终都会遇到同一个问题:怎么让别人在终端敲一行pip install something,就能把你的代码装进他的环境里。这就是我这次要聊的主题——python包发布流程。发布包这件事,表面上看只是上传几个文件,实际背后涉及工程结构、构建工具、元数据规范、账号权限管理和版本迭代策略。很多本地跑得好好的项目,一走到发布环节就各种报错,根子往往不是代码问题,而是流程没梳理清楚。
这篇内容我会从零开始,用一个具体的示例包走完整个发布流程:先讲清楚为什么需要打包、选择哪种构建方式,然后把项目骨架和pyproject.toml配置逐项拆开,再实操构建出 wheel 包和源码包,最后上传到 TestPyPI 做预发布验证,确认无误后正式发布到 PyPI。无论你用的是 VS Code 还是 PyCharm,构建发布和编辑器本身没关系,只要终端里能正常调用python和pip,这套流程就能跑通。适合所有想把代码分享给更多人使用的 Python 开发者,也适合团队内部需要统一分发工具的运维和研发同学。
1. 发布前想清楚:你的包到底要解决什么问题,怎么分发最合适
1.1 不是每个项目都需要发布成完整包
很多初学者有个误区,觉得自己写了一堆 .py 文件,把它们压缩成 zip 发给同事,就算是“发布了”。这种方式的痛处很明显:对方拿到压缩包得自己解压、配环境、手动设置 PYTHONPATH,依赖关系全靠聊天记录维护。我见过不少团队内部工具就是靠网盘传版本,最后所有人都不知道自己装的是哪一版,出了问题根本没法回溯。
判断一个项目是否需要走完整发布流程,我一般问三个问题:这个代码是否会被多个项目复用?使用它的人是否不熟悉项目内部结构?是否需要一个明确的版本号来追踪变更?如果三个里至少占两个,那就值得打成标准安装包。反之,如果只是一个脚本里的辅助函数,那直接复制文件或者用 Git 子模块反而更合理。打包发布是有成本的,分清边界能省不少维护精力。
1.2 构建后端怎么选:setuptools、hatchling 还是 poetry
Python 生态里构建后端不是只有一个选择,目前主流的有 setuptools、hatchling、flit、poetry 等。很多人一上来就纠结“哪个最好”,其实对大多数项目来说,setuptools 是最稳的默认选项。它的生态最成熟,第三方文档多,踩坑经验也最容易搜到,更重要的是从传统的 setup.py 迁移到 pyproject.toml 时几乎不用改业务代码。
hatchling 是这两年很受推荐的轻量后端,配置更简洁,构建速度也快,适合新起步且没有历史包袱的项目。poetry 则是把依赖管理和打包发布绑定在一个命令行工具里,体验很好,但如果你的团队其他人不熟悉 poetry,协作成本会上升。我个人的选择标准很简单:团队已经在用某个工具就继续用,别为了追赶潮流折腾;如果是个人项目或者教学演示,默认 setuptools,等真正需要更细粒度控制再去研究其他后端。
1.3 先跑通本地安装,再谈远程发布
正式上传之前,必须先确认这个包在本地可以被 pip 正常安装。最常用的命令是:
python -m venv .venv source .venv/bin/activate # Windows 下是 .venv\Scripts\activate pip install -e .这里的-e表示 editable 模式,也就是“可编辑安装”,安装后你改源码会立即生效,非常适合开发调试。如果这一步能过,说明项目结构和包发现配置没有大问题,之后再谈构建分发文件才有意义。很多发布失败的案例,根源就是开发者跳过本地安装验证,直接python setup.py sdist上传,结果对方装上后 module 都导入不了。
2. 创建合格的工程骨架:从 pyproject.toml 到 README 一个都不能少
2.1 项目目录结构:直接照抄这份模板
不管包多简单,建议都采用 src 布局。所谓 src 布局,就是把真正的 Python 代码放在src/目录下,而不是项目根目录下。这样能避免一个经典问题:当你项目根目录恰好有个跟包名一样的目录时,运行测试时 Python 可能导入的是项目根目录下的源码,而不是你安装好的版本,两者不一致会导致莫名其妙的 bug。
下面是一个基础但规范的模板,可以直接套用:
demo-pkg/ ├── pyproject.toml ├── README.md ├── LICENSE ├── .gitignore └── src/ └── demo_pkg/ ├── __init__.py └── core.py注意包名demo_pkg用的是下划线,而项目名demo-pkg用的是短横线,PyPI 规范允许短横线,但实际导入模块时 Python 只能用下划线。这个细节几乎每个新人都踩过,你先记住,后面配置里会用到。
2.2 pyproject.toml 配置解析:每个字段都在干什么
pyproject.toml 是 PEP 621 规定的项目配置文件,现在构建工具都会优先读取它。我贴一份可用度很高的配置,然后逐行解释:
[build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] name = "demo-pkg" version = "0.1.0" description = "A demo package for publishing practice" readme = "README.md" requires-python = ">=3.8" license = { text = "MIT" } authors = [ { name = "Your Name", email = "your@example.com" } ] keywords = ["demo", "packaging", "tutorial"] classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] dependencies = [ "requests>=2.25,<3.0", ] [project.optional-dependencies] dev = [ "pytest", "build", "twine", ] [tool.setuptools.packages.find] where = ["src"][build-system]声明了构建此项目所需的工具和入口,这样 pip 在安装时就知道要用 setuptools 来构建。name是 PyPI 上显示的项目名,必须全局唯一,发布前记得先去 pypi.org 搜索一下有没有同名项目。version是发布版本号,PyPI 不允许重复上传同一个版本,每次更新必须递增。requires-python声明支持的 Python 最低版本,这个字段会影响 pip 的依赖解析。license可以直接声明文本,更规范的做法是引用 LICENSE 文件。classifiers是给 PyPI 做分类检索用的元数据,不填也能发布,但填了更容易被搜索到。
dependencies是运行时依赖,[project.optional-dependencies]是可选依赖,比如把构建和测试工具放到 dev 组里。最后的[tool.setuptools.packages.find]配合where = ["src"],告诉 setuptools 从 src 目录下自动发现包,这也是 src 布局能正常工作的关键配置。
2.3 README、LICENSE 和元数据:影响的不只是首页展示
很多人觉得 README 是可有可无的凑数文件,实际上 PyPI 会把 README 渲染成包的主页,一个没有说明文档的包给用户的信任感会大打折扣。在 pyproject.toml 里写了readme = "README.md"之后,构建工具会自动把 README 内容嵌入到包的元数据中,用户装好后也可以通过help(包名)看到。
LICENSE 文件同样重要,它决定了别人能不能合法复用你的代码。个人练手项目推荐 MIT,开源但不保留太多限制;商业项目则要根据公司法务要求来定。顺带一提,很多构建工具在twine check阶段会给出缺少 LICENSE 的警告,虽然不是致命错误,但最好别带着警告发布。
2.4 .gitignore:别把构建产物和虚拟环境提交上去
项目里一定要有.gitignore,至少忽略这些内容:
__pycache__/ *.py[cod] *.egg-info/ build/ dist/ .venv/如果不忽略dist/和*.egg-info/,这些构建产生的临时产物一旦进入 Git 历史,后面很容易出现版本混乱。特别是*.egg-info里记录了构建时的路径信息,如果换台机器重新构建,陈旧的 egg-info 可能导致 setuptools 发现错误的包结构。我处理过不止一次“本地能跑但 CI 上装不上”的问题,最后定位到就是仓库里残留了旧 egg-info。
3. 从代码到安装包:构建分发文件的核心实操
3.1 准备构建环境:安装 build 和 twine
构建分发文件需要用到两个官方工具:build负责把项目打包成发行文件,twine负责校验和上传。安装方式很简单:
pip install build twine这里不用python setup.py sdist这种老式命令,因为新构建后端统一走 PEP 517/660 标准。你只要执行python -m build,它会根据 pyproject.toml 里的 build-backend 自动完成源码包和 wheel 包的构建。这样做的好处是构建行为更标准化,不会因为你本地多装了什么包而产生额外差异。
3.2 执行构建:源码包和 wheel 包分别是什么
在项目根目录执行:
python -m build正常情况下会生成dist/目录,里面有类似这两个文件的东西:
dist/ ├── demo_pkg-0.1.0-py3-none-any.whl └── demo_pkg-0.1.0.tar.gz以.tar.gz结尾的是源码包(sdist),它包含源码、配置文件和构建所需的全部信息,适合在无法直接安装 wheel 的环境里从源码构建。以.whl结尾的是wheel 包,本质上是一个 zip 压缩包,pip 可以直接解压安装,不需要再执行构建步骤,安装速度更快。两者都要上传,因为有些用户或 CI 系统只认其中一种。
关于 wheel 文件名里的py3-none-any,py3表示兼容 Python 3,none表示不依赖特定 ABI,any表示平台无关。如果你的包里有 C 扩展,这里的标签会变化,但纯 Python 项目基本都会生成这个标签。
3.3 发布前检查:别把坏文件传上去
上传之前务必跑一下twine check:
python -m twine check dist/*这个命令会检查发行文件的元数据是否符合 PyPI 要求,比如 README 能否正常渲染、字段是否缺失。如果输出里出现WARNING和ERROR,一般不建议直接上传。常见的错误里有 long_description 格式不对,以及使用 Markdown 但没在 pyproject.toml 里声明readme = "README.md"导致的渲染问题。
python -m twine check dist/* Checking distribution dist/demo_pkg-0.1.0-py3-none-any.whl: Passed Checking distribution dist/demo_pkg-0.1.0.tar.gz: Passed看到 Passed 就可以继续了。还可以解压 wheel 看一眼里面到底有什么,确认没有误打包进无关目录:
unzip -l dist/demo_pkg-0.1.0-py3-none-any.whl这一步很容易发现哪些不该出现的文件混进来了,比如本地的.env或者过大的测试数据文件。
4. 测试与发布:用 TestPyPI 练手,再正式上线 PyPI
4.1 注册账号并生成 API Token,而不是用密码
PyPI 账号在 pypi.org 注册,TestPyPI 账号需要在 test.pypi.org 单独注册一次,两者不通用。登录后进入 Account settings,在 API tokens 区域创建一个 token。权限建议选择Scope: entire account,这样以后任何新项目都能用同一个 token 上传,不用每加一个包就重新生成。
这里有个安全习惯必须强调:不要把 token 明文写在项目代码里,也不要分享给不相关的人。token 一旦泄露,其他人就能以你的名义上传恶意版本,伪装成你的包投毒。所以上传命令里我强烈建议配合环境变量使用,比如在 shell 里先设置:
export TWINE_USERNAME=__token__ export TWINE_PASSWORD=你的token或者把常用的仓库配置写进~/.pypirc:
[distutils] index-servers = pypi testpypi [pypi] repository = https://upload.pypi.org/legacy/ username = __token__ password = 你的token [testpypi] repository = https://test.pypi.org/legacy/ username = __token__ password = 你的token注意 password 字段填的是完整的 token,而不是明文密码。用户名固定写__token__,这是 PyPI 规定的特殊用户名,用来标识 API token 登录。
4.2 上传到 TestPyPI:完整演练一遍发布动作
正式发布前,一定在 TestPyPI 上完整走一遍,这就像正式上线前的预发布环境。执行:
python -m twine upload -r testpypi dist/*-r指定仓库名称,对应.pypirc里配置的testpypi。上传成功后,你会看到一个 URL 指向 test.pypi.org 上的项目页面。然后新建一个干净的虚拟环境,从 TestPyPI 安装验证:
pip install --index-url https://test.pypi.org/simple/ demo-pkg如果包本身依赖其他 PyPI 包,安装时容易遇到依赖找不到的问题,因为--index-url替换掉了默认的 PyPI 源。这种情况需要同时指定官方源:
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ demo-pkg在干净环境里成功导入包、执行核心函数后,预发布验证才算完成。
4.3 正式上传到 PyPI:执行前再核对版本号
TestPyPI 验证通过后,正式发布就只有一条命令的事:
python -m twine upload dist/*默认会上传到 pypi 仓库。输入这条命令前,养成习惯再核对三个点:版本号是否已经递增;dist 目录里是否只有本次构建的文件,没有混进上一个版本的存档;twine check是否已经通过。我吃过一次亏,dist 目录里还留着旧版本文件,结果 twine 把旧文件也一并上传了,PyPI 直接拒绝,日志报着 File already exists 的错,排查半天才意识到目录没清理。
正式发布成功后,回到干净环境执行pip install demo-pkg验证。如果一切正常,你会发现几分钟前刚上传的包已经可以被正常安装了。
4.4 版本更新策略:一次发布就是一个不可变的快照
很多人第一次发布后,发现有个小 bug,于是想“赶紧重新上传一个同名文件覆盖掉”。PyPI 不允许这样做。一个版本一旦上传,内容就是不可变的,你只能通过递增版本号的方式发布新版本。所以版本号管理很关键,我习惯遵循语义化版本规范:主版本号在 API 不兼容时递增,次版本号在新增功能时递增,修订号在修 bug 时递增。
version = "0.1.0" # 修复 bug 后 version = "0.1.1" # 新增功能后 version = "0.2.0" # 发布候选版本 version = "0.3.0rc1"候选版本用rc1后缀,发布后正式版再递增为0.3.0,用户装正式版时 pip 会优先选择正式版而不是候选版。
5. 自动化与团队协作:让发布流程不再依赖某个人
5.1 用 GitHub Actions 自动构建并发布
如果项目托管在 GitHub,可以配置 Actions 在打 tag 时自动构建上传,彻底解放手动操作。下面是一个精简但完整的 workflow:
name: Publish to PyPI on: push: tags: - "v*" jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: "3.11" - name: Install dependencies run: | pip install build twine - name: Build package run: python -m build - name: Publish to PyPI run: twine upload dist/* env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}这个 workflow 在推送v*标签时触发,比如打v0.1.0标签。关键是把 token 存到 GitHub 仓库的 Settings -> Secrets 里,变量名PYPI_API_TOKEN,这样既自动化又没有把密钥暴露到代码里。引入自动化之后,发布的动作就变成“打 tag”和“等几分钟”,谁都能操作,不再依赖某个有本机 token 的同学。
5.2 团队协作时的版本与分支约定
多人协作发布时,最怕大家各发各的版本,导致 PyPI 上的版本号乱了套。我见过一个团队的内部库,版本号出现1.2.3、1.2.3.final、1.2.3fix,后来依赖解析直接炸掉。建议在团队文档里写死三条规则:只有主分支代码通过测试后才能发布;所有版本号递增操作必须在 pyproject.toml 中单独提交;发布操作只允许负责人执行,其他人提交变更即可。
还可以在 CI 里加一个检查,每次合并代码时验证 pyproject.toml 中的版本号是否高于当前已发布的最新版本。办法不复杂,用一个 Python 脚本调用 pypi.org 的 JSON API 拉取最新版本,再和本地版本对比。这样能防止有人忘记递增版本就合并,把发布环节的隐患消灭在源头。
5.3 常见问题排查清单
我整理了发布过程中最常遇到的几类问题,方便你按表快速定位。
| 错误现象 | 常见原因 | 解决办法 |
|---|---|---|
401 Invalid or non-existent authentication information | token 写错、过期,或用了用户名密码 | 检查.pypirc和环境变量里的 token,重新生成 API token |
403 The user xxx is not allowed to upload to project | 当前账号不是该项目的维护者 | 确认登录的是项目 owner 账号,或联系 owner 添加 collaborator |
File already exists | 同一版本号重复上传 | 递增 version 后重新构建,或先清理 dist 再上传 |
README can't be rendered | long_description 格式或编码问题 | 确认 pyproject.toml 中readme字段指向正确文件,执行twine check查看具体错误 |
| 发布成功后 pip 找不到包 | 包名拼写不一致,或在 TestPyPI 验证时用了错误索引源 | 核对 PyPI 页面上的真实项目名,安装时确认--index-url或默认源配置 |
Package would be ignored | 版本号或 wheel 标签不符合规范 | 检查 version 是否为合法 PEP 440 格式,wheel 文件名是否包含正确的 Python 标签 |
这七类问题覆盖了我经手过的大部分发布故障。你如果遇到不在表里的报错,最直接的办法是把完整错误信息复制到搜索引擎里搜,比凭感觉猜原因高效得多。
发布 Python 包这件事,看起来步骤多,但核心闭环就四个字:构建、校验、上传、验证。构建用python -m build,校验用twine check,上传用twine upload,验证就是在干净环境里pip install自己的包。我每次教新人,都要求他们先在 TestPyPI 上完整走一遍流程,宁可多花二十分钟预演,也别把错误直接暴露给真实用户。最后再分享一个亲测有效的小技巧:发布后立刻用一台没装过这个包的环境安装一遍,别用你日常开发环境凑合验证。日常环境里早就残留了旧的包文件或者路径配置,根本测不出真实用户会遇到的问题。这个习惯能帮你躲过绝大多数“发布即翻车”的尴尬场景,值得坚持。