从裸脚本到包:为什么你该有 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