不管你是写爬虫脚本、量化策略还是做数据可视化工具最终都会遇到同一个问题怎么让别人在终端敲一行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 还是 poetryPython 生态里构建后端不是只有一个选择目前主流的有 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 [setuptools61.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 yourexample.com } ] keywords [demo, packaging, tutorial] classifiers [ Programming Language :: Python :: 3, License :: OSI Approved :: MIT License, Operating System :: OS Independent, ] dependencies [ requests2.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-anypy3表示兼容 Python 3none表示不依赖特定 ABIany表示平台无关。如果你的包里有 C 扩展这里的标签会变化但纯 Python 项目基本都会生成这个标签。3.3 发布前检查别把坏文件传上去上传之前务必跑一下twine checkpython -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 练手再正式上线 PyPI4.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 时自动构建上传彻底解放手动操作。下面是一个精简但完整的 workflowname: Publish to PyPI on: push: tags: - v* jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - uses: actions/setup-pythonv4 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 informationtoken 写错、过期或用了用户名密码检查.pypirc和环境变量里的 token重新生成 API token403 The user xxx is not allowed to upload to project当前账号不是该项目的维护者确认登录的是项目 owner 账号或联系 owner 添加 collaboratorFile already exists同一版本号重复上传递增 version 后重新构建或先清理 dist 再上传README cant be renderedlong_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 上完整走一遍流程宁可多花二十分钟预演也别把错误直接暴露给真实用户。最后再分享一个亲测有效的小技巧发布后立刻用一台没装过这个包的环境安装一遍别用你日常开发环境凑合验证。日常环境里早就残留了旧的包文件或者路径配置根本测不出真实用户会遇到的问题。这个习惯能帮你躲过绝大多数“发布即翻车”的尴尬场景值得坚持。