从入门到精通:Python项目打包与setup.py实战指南
前言setup.py曾经是 Python 项目打包的唯一入口。你在项目根目录放一个setup.py调用里面的setup()函数声明项目信息然后跑python setup.py install就能装到环境里。这套流程统治了十几年网上大量教程至今仍在教它。但它的时代基本结束了而且结束得很硬Python 3.12 起标准库里的distutils包已被移除PEP 632。老代码里那句from distutils.core import setup在 3.12 上会直接抛ModuleNotFoundError: No module named distutils。与此同时python setup.py install这类直接调用的方式早已被官方标记为不推荐取而代之的是基于 PEP 517 / 518 / 621 的「声明式配置 构建前端」模式。本文按「入门 — 进阶 — 精通」三段走先讲清setup.py是什么、为什么还能看到它再讲如何把它迁移到声明式配置最后讲现代的构建流程与产物。本文给的是迁移路径不是让你继续用老命令。一、入门setup.py 是什么setup.py是一个 Python 脚本它调用打包工具的setup()函数来声明项目信息。最老一版的写法从distutils导入# 适用于 Python 3.11 及更早版本Python 3.12 起 distutils 已被移除此写法会报错from distutils.core import setupsetup(namedemo-lib,version0.1.0,py_modules[demo_lib],)后来人们普遍改用setuptools它功能更强支持依赖声明、入口点等# 适用于 Python 3.8需已安装 setuptools但已不是推荐写法from setuptools import setup, find_packagessetup(namedemo-lib,version0.1.0,packagesfind_packages(),python_requires3.9,install_requires[requests2.31],entry_points{console_scripts: [demo-clidemo_lib.cli:main,],},)find_packages()会自动找出当前目录下的包含__init__.py的目录。install_requires声明运行依赖python_requires声明支持的 Python 版本entry_points里的console_scripts用来生成命令行入口。这套写法今天仍然能跑前提是环境里装了setuptools。它的问题是把「项目元数据」和「构建逻辑」混在一个可执行的 Python 脚本里构建过程因此变得不可控——脚本能做的事太多了。二、为什么 distutils 没了distutils是早期随 Python 一起分发的打包库setuptools最初就是在它之上扩展出来的。两条线长期并存职责重叠维护负担却分散。PEP 632 决定把它从标准库移除Python 3.10 起标记为废弃Python 3.12 起正式移除标准库里不再有distutils官方文档明确说明「从 Python 3.12 起导入distutils的代码将不再工作」对实在无法迁移的项目官方给出的过渡办法是安装setuptools——它仍然提供一个distutils供旧代码使用。# 在 Python 3.12 上运行旧的 distutils 写法时会看到类似报错ModuleNotFoundError: No module named distutils看到这个报错正确的反应不是去找一个distutils的第三方复刻包而是把项目迁移到pyproject.toml。setuptools提供的distutils兼容层只是给迁移拖延时间的不该当作长期方案。三、进阶从脚本到声明式配置第一步迁移是「声明与逻辑分离」把setup()的关键字参数搬进setup.cfg让setup.py退化成一行调用。这是一种过渡形态。# setup.cfg声明式配置[metadata]name demo-libversion 0.1.0description 一个演示用的小库[options]packages find:python_requires 3.9install_requires requests2.31[options.entry_points]console_scripts demo-cli demo_lib.cli:main# 适用于 Python 3.8需已安装 setuptools仅用于兼容老工具链from setuptools import setupsetup()这样做的好处是配置变成纯数据不再有可执行逻辑但setup.cfg是setuptools自己的格式不是所有构建后端都认。再往前一步就该用跨后端的pyproject.toml了。四、精通pyproject.toml 与 PEP 621pyproject.toml是现在的标准配置文件。它由几个 PEP 共同定义PEP引入的内容PEP 518[build-system]表声明构建依赖与构建后端PEP 517定义构建后端的标准接口前端不再需要知道后端细节PEP 621规范[project]表项目元数据的标准字段# pyproject.toml现代写法[build-system]requires [setuptools61]build-backend setuptools.build_meta[project]name demo-libversion 0.1.0description 一个演示用的小库readme README.mdrequires-python 3.9dependencies [requests2.31,][project.optional-dependencies]dev [pytest7][project.scripts]demo-cli demo_lib.cli:main[tool.setuptools.packages.find]where [src]对比一下迁移前后的对应关系setup.py里的写法pyproject.toml里的写法namedemo-lib[project]的nameversion0.1.0[project]的versioninstall_requires[...][project]的dependenciespython_requires3.9[project]的requires-pythonentry_points{console_scripts: [...]}[project.scripts]packagesfind_packages()[tool.setuptools.packages.find]extras_require{dev: [...]}[project.optional-dependencies]迁移之后setup.py可以整个删掉。构建命令也随之变化——不再调用setup.py而是用构建前端。五、构建与验证现代流程用 PyPA 的build工具第三方工具不是标准库在隔离环境里调用后端构建# 安装构建工具python -m pip install build# 构建产物在 dist/python -m builddist/下会得到 wheel.whl和源码包.tar.gz。随后在干净环境里装 wheel 验证python -m pip install dist/demo_lib-0.1.0-py3-none-any.whl# 验证包能导入、命令行入口能用python -c import demo_lib; print(demo_lib.__name__)demo-cli 3 4不要再执行python setup.py install。这个命令绕过了构建前端和隔离机制会把中间文件留在源码目录也得不到可复现的产物。同理python setup.py sdist、python setup.py bdist_wheel这类直接调用也应替换为python -m build。推荐配套的 src 布局demo-lib/├── pyproject.toml├── README.md└── src/└── demo_lib/├── __init__.py├── core.py└── cli.py代码放在src/demo_lib/配置里通过[tool.setuptools.packages.find]的where [src]告诉工具去哪里找包。这种布局强制「必须先安装才能导入」能提前暴露打包遗漏。六、迁移实战一个完整的小项目# 适用于 Python 3.9文件 src/demo_lib/core.pydef add(a, b):返回两数之和。return a b# 适用于 Python 3.9文件 src/demo_lib/__init__.pydemo_lib演示从 setup.py 迁移到 pyproject.toml。__version__ 0.1.0from .core import add__all__ [add, __version__]# 适用于 Python 3.9文件 src/demo_lib/cli.pyimport sysfrom .core import adddef main(argvNone):argv sys.argv[1:] if argv is None else argvif len(argv) ! 2:print(用法demo-cli 数字1 数字2)return 2try:left, right (int(x) for x in argv)except ValueError:print(两个参数都必须是整数)return 2print(add(left, right))return 0if __name__ __main__:raise SystemExit(main())逐行推演__init__.py用相对导入from .core import add把函数提到包的名字空间外部from demo_lib import add即可使用。cli.py的main默认读命令行参数允许调用方传入列表参数个数不对或无法转成整数时打印提示并返回退出码 2成功时打印结果并返回 0。raise SystemExit(main())把返回码交给操作系统。迁移步骤按顺序是# 1. 新建 pyproject.toml把 setup.py 的字段翻译过去# 2. 代码挪进 src/ 布局可选但推荐# 3. 删掉 setup.py确认配置完整的前提下# 4. 构建并验证python -m pip install buildpython -m buildpython -m pip install dist/demo_lib-0.1.0-py3-none-any.whldemo-cli 3 4第 4 步打印 7说明[project.scripts]的入口配置生效。至此这个项目已经不再依赖setup.py。常见坑点坑点 1在 3.12 上照抄from distutils.core import setup。❌ 老教程里的这行在Python 3.12 及以上直接抛ModuleNotFoundError: No module named distutilsPEP 632 已把 distutils 移出标准库。 ✅ 迁移到pyproject.toml加setuptools后端只有临时救急时才依赖setuptools提供的distutils兼容层。坑点 2继续用python setup.py install。❌ 这条命令绕过构建前端与隔离产物不可复现中间文件堆在源码目录。 ✅ 用python -m build构建再python -m pip install dist/xxx.whl安装。坑点 3以为setup.cfg是通用的跨后端标准。❌ 把全部配置写成setup.cfg换构建后端后配置读不出来——它是setuptools的格式不是跨后端的。 ✅ 元数据放pyproject.toml的[project]PEP 621只有 setuptools 特有的细节才放进[tool.setuptools]。坑点 4[build-system]的requires与build-backend对不上。❌requires里没写对应后端包或后端名拼错构建时报找不到后端。 ✅ 二者保持一致用 setuptools 时requires [setuptools61]、build-backend setuptools.build_meta。坑点 5删了 setup.py 却没把字段搬全。❌ 删掉setup.py前忘了迁python_requires或entry_points装完发现版本不受限、命令行入口不存在。 ✅ 按前文的对应关系表逐条核对尤其是requires-python与[project.scripts]。坑点 6src 布局下忘了配置查找路径。❌ 代码放src/demo_lib/但配置里没有[tool.setuptools.packages.find]的where [src]构建出来的 wheel 里没有包。 ✅ 补上where [src]构建后解包检查 wheel 内容确认包在内。坑点 7以为build和twine随 Python 附带。❌ 没安装就python -m build——抛No module named build。 ✅ 先python -m pip install build要上传再python -m pip install twine。两者都是 PyPA 的第三方工具。坑点 8只在源码目录里测从没真正装过。❌ 一直靠python -c import demo_lib因为恰好在项目根目录验证发布后用户安装却导入失败。 ✅ 构建 wheel、在干净环境里安装后再验证导入与入口这才是打包的最终验收。总结阶段配置载体构建方式状态最老setup.pydistutilspython setup.py install3.12 起 distutils 已移除过渡setup.pysetuptoolspython setup.py ...仍可运行但不推荐半声明式setup.cfg 空setup.py同上setuptools 专有格式现代pyproject.tomlPEP 621python -m build推荐从setup.py到pyproject.toml本质上是从「可执行脚本」走向「声明式数据 标准构建接口」。理解了这个转向就明白了为什么老教程里的命令今天会报错也知道了迁移该往哪个方向走把元数据写进[project]把构建后端写进[build-system]用python -m build出产物用干净环境安装验证。这四步走完项目就站到了现代打包的地基上。