编程语言

Python 打包与依赖锁定:从 pyproject.toml 到发布 PyPI

面向把脚本落地成可发布包的 Python 工程:用 pyproject.toml 管理元信息、venv 隔离、锁定文件复现环境、twine 发布到 PyPI。

作者:巧匠团队·8 分钟阅读·更新于 2026-09-06

从裸脚本到包:为什么你该有 pyproject.toml

.py 脚本丢给别人跑,对方缺依赖、版本不一致、装不进环境,问题频出。pyproject.toml 集中声明名称、版本、入口、依赖范围,是所有现代工具的统一描述文件。

它至少承载三件事:构建后端声明(build-system)、项目元信息(name/version/dependencies)、还可能含工具配置(ruff、black、pytest 段落)。

后端我用 setuptools 或 hatchling 都可以,两者都在 pyproject.toml 里配置,差别在风格与自动版本集生成,选熟人熟悉的即可。

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

[project]
name = "mypkg"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
  "requests>=2.31",
  "click>=8.0",
]

[project.optional-dependencies]
dev = ["pytest>=8", "ruff>=0.4"]

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

[tool.setuptools.packages.find]
where = ["src"]

venv + 锁定文件:让环境可复现

pyproject.toml 只管依赖的“上界范围”,同一份配置在不同机器可能装出不同版本组合。要可复现得靠锁定文件(lock)把每个依赖钉到具体版本。

传统做法用 pip freeze > requirements.txt,但那是“已装环境”快照,易混入开发杂项;推荐用 uv、poetry、或 pip-tools,它们从 pyproject 解析出 lock,并区分 dev/prod。

本地开发始终在虚拟环境(venv / .venv)里装,锁文件随仓库提交,别人或 CI 拉下来后一条命令还原出与开发完全一致的依赖。

# 用 uv 锁依赖(示例)
python -m venv .venv
.venv/Scripts/activate        # Windows
# source .venv/bin/activate   # macOS/Linux

uv pip compile pyproject.toml -o requirements.lock
uv pip sync requirements.lock

# pip-tools 等价
# pip-compile pyproject.toml -o requirements.lock
# pip-sync requirements.lock

错误示范:依赖写死 ==1.2.3 反而埋雷

硬编码 ==1.2.3 在 pyproject 里会导致:新机器无法解析出安全补丁、传递依赖版本漂移(主依赖写死但传递依赖仍浮动)、多项目共用同一包时版本互顶。

正解是分层声明:pyproject 里写有意义的范围(>=1.2, <1.3 或 ~=1.2),让解析器在范围内挑适合的;锁定文件单独维护精确版本,保证可复现。

升级时改 lock 与 pyproject 一起,跑测试后再提交,避免“版本改了没人验证”的静默漂移。

# 错误示范:把写死的精确版本放进元数据
# dependencies = ["requests==2.31.0", "pydantic==2.5.3"]

# 修复:范围声明放 pyproject,精确版本交给 lock
[project]
dependencies = [
  "requests~=2.31.0",
  "pydantic>=2.5,<3.0",
]

# lock 文件里才写精确值:
# requests==2.31.0
# pydantic==2.5.3

本地构建与 PyPI 发布:从 sdist/wheel 到 twine

发布前先在本地构建出可验证的产物:wheel(通用/平台特定)与 sdist(源码包)。用 python -m build 一次性生成两者,别只发 wheel 而漏了 sdist。

用 twine 上传到 TestPyPI 或正式 PyPI。正式发布前先检查 README 渲染、license 与版本号不重复(同版本不能重传)。

用 python -m build、twine check 一步步来,配合 .pypirc 或环境变量,账号 token 尽量进环境或密码管理器,不要硬编码进脚本。

# 本地构建
python -m build
# dist/ 生成 .whl 与 .tar.gz

# 预检产物
twine check dist/*

# 上传到 PyPI(账号 token 由环境提供)
export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="$PYPI_API_TOKEN"
twine upload dist/*

# 想先上 TestPyPI:
# twine upload --repository testpypi dist/*

验证发布可从干净环境安装使用

发布不等于完成,还要证明“从 PyPI 拉到全新环境能装、能跑”。这是很多人跳过的一环。

用临时 venv 只装你发布的 wheel/sdist,import 与 CLI 都冒一次烟;若声明了 entry 脚本,直接执行入口命令。

给发布系统设 CI 的 publish 步骤同样该验证,避免“让我手动在容器里装一遍才能上线”的不必要摩擦。

# 干净环境一次性验证
python -m venv /tmp/verify
/tmp/verify/Scripts/activate      # Windows
# source /tmp/verify/bin/activate # macOS/Linux

pip install mypkg==0.1.0
python -c "import mypkg; print(mypkg.__version__)"
mycli --help            # CLI 入口冒烟

pip list | grep mypkg

官方参考来源

下方为命令对应的官方权威文档,供你核对最新用法与深入查阅。