Python项目打包与发布的三大工具(setuptools、Poetry、Flit)使用完全指南
作者:铭渊老黄
引言:当你的代码值得被世界看见
每个 Python 开发者都有这样的时刻——你写了一个解决实际问题的工具,同事们用了都说好,你开始思考:能不能让更多人用上它?
这就是打包与发布的起点。
但随之而来的困惑往往让人望而却步:setup.py、pyproject.toml、MANIFEST.in、wheel、sdist……这些概念是什么?setuptools、Poetry、Flit 又该选哪个?如何把包发布到 PyPI?
我做了十几年 Python 开发,亲历了从手写 setup.py 到现代 pyproject.toml 的整个演变。今天这篇文章,就是我希望当年入门时就能读到的那篇指南——带你从零到一,真正搞清楚 Python 打包这件事。
第一章:先理解"打包"究竟是什么
在动手之前,先建立清晰的概念模型。
1.1 为什么需要打包
你的项目目录
├── mylib/
│ ├── __init__.py
│ ├── core.py
│ └── utils.py
├── tests/
├── README.md
└── requirements.txt
别人想用你的库,直接复制文件夹?显然不够优雅。打包解决了这些问题:
- 依赖声明:我需要哪些第三方库,版本是多少?
- 元数据:作者、版本号、许可证、描述……
- 分发格式:让
pip install能直接安装 - 命令行工具:如果你的包提供 CLI,如何注册入口点?
1.2 两种分发格式
发布产物
├── mylib-1.0.0.tar.gz ← sdist(源码发行包)
└── mylib-1.0.0-py3-none-any.whl ← wheel(二进制发行包)
- sdist:源码包,安装时需要在用户机器上构建
- wheel:预构建包,安装速度更快,是现代推荐格式
1.3 配置文件的演变
Python 打包配置的历史时间线
2000s setup.py(纯 Python 脚本,灵活但混乱)
│
2012 setup.cfg(声明式配置,分离逻辑与元数据)
│
2016 PEP 518 提出 pyproject.toml(统一的配置标准)
│
2021+ pyproject.toml 成为主流,各工具纷纷支持
现在的最佳实践:使用 pyproject.toml 作为主配置文件,告别 setup.py。
第二章:三大工具横向对比
2.1 Setuptools——老牌权威
适用场景:复杂项目、需要编译扩展(C/C++ 扩展)、需要高度自定义构建过程
# pyproject.toml(现代 setuptools 写法)
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.backends.legacy:build"
[project]
name = "myawesome-lib"
version = "1.0.0"
description = "一个很棒的 Python 库"
readme = "README.md"
license = {text = "MIT"}
authors = [
{name = "Your Name", email = "you@example.com"}
]
requires-python = ">=3.8"
dependencies = [
"requests>=2.28.0",
"click>=8.0",
]
[project.optional-dependencies]
dev = ["pytest>=7.0", "black", "mypy"]
docs = ["sphinx", "sphinx-rtd-theme"]
[project.scripts]
mylib-cli = "mylib.cli:main"
[project.urls]
Homepage = "https://github.com/yourname/mylib"
Documentation = "https://mylib.readthedocs.io"
对应的项目结构:
myawesome-lib/
├── pyproject.toml
├── README.md
├── LICENSE
├── src/
│ └── mylib/ ← 推荐 src 布局
│ ├── __init__.py
│ ├── core.py
│ └── cli.py
└── tests/
└── test_core.py
为什么推荐 src 布局?它能防止你在开发时意外导入本地未安装版本,让测试更加可靠。
优点:生态最成熟,文档丰富,支持所有复杂场景
缺点:配置项繁多,学习曲线稍陡,依赖管理需配合 pip-tools 等工具
2.2 Poetry——现代全能选手
适用场景:日常 Python 项目,既要管理依赖又要打包发布,追求开发体验
Poetry 是目前我最推荐的工具,原因很简单:它把虚拟环境管理、依赖管理、打包发布整合成了一套流畅的工作流。
# 创建新项目
poetry new myawesome-lib
# 目录结构自动生成
myawesome-lib/
├── pyproject.toml
├── README.md
├── myawesome_lib/
│ └── __init__.py
└── tests/
└── __init__.py
生成的 pyproject.toml:
[tool.poetry]
name = "myawesome-lib"
version = "0.1.0"
description = "一个很棒的 Python 库"
authors = ["Your Name <you@example.com>"]
readme = "README.md"
license = "MIT"
homepage = "https://github.com/yourname/myawesome-lib"
repository = "https://github.com/yourname/myawesome-lib"
keywords = ["python", "library"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
]
[tool.poetry.dependencies]
python = "^3.8"
requests = "^2.28.0"
click = "^8.0"
[tool.poetry.group.dev.dependencies]
pytest = "^7.0"
black = "^23.0"
mypy = "^1.0"
[tool.poetry.scripts]
mylib-cli = "myawesome_lib.cli:main"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
核心工作流:
# 添加依赖(自动更新 pyproject.toml 和 poetry.lock) poetry add requests # 添加开发依赖 poetry add --group dev pytest black # 安装所有依赖(包含 lock 文件确保版本一致) poetry install # 运行项目内命令 poetry run python main.py poetry run pytest # 构建发行包 poetry build # 输出:dist/myawesome_lib-0.1.0.tar.gz # dist/myawesome_lib-0.1.0-py3-none-any.whl # 发布到 PyPI(一键完成) poetry publish
依赖版本控制语义:
[tool.poetry.dependencies] requests = "^2.28.0" # 兼容 2.x,不升级到 3.x click = "~8.0.0" # 只允许补丁版本升级 numpy = ">=1.20,<2.0" # 区间限定 flask = "*" # 任意版本(不推荐)
优点:一体化体验、lock 文件保证可复现、依赖解析强大、发布流程简洁
缺点:不支持 C 扩展编译、相对较重(但绝大多数项目不需要编译扩展)
2.3 Flit——极简主义者的选择
适用场景:纯 Python 库、结构简单、希望配置尽量少
# pyproject.toml
[build-system]
requires = ["flit_core>=3.4"]
build-backend = "flit_core.buildapi"
[project]
name = "mylib"
dynamic = ["version", "description"] # 从代码中读取
readme = "README.md"
license = {file = "LICENSE"}
authors = [{name = "Your Name", email = "you@example.com"}]
requires-python = ">=3.8"
dependencies = ["requests"]
版本号和描述直接从模块文件中读取:
# mylib/__init__.py """一个极简的 Python 库。""" # 这是包的 description __version__ = "1.0.0" # 这是版本号
# 构建 flit build # 发布 flit publish
优点:配置极少,上手快,适合小型纯 Python 包
缺点:功能有限,不做依赖管理,复杂项目力不从心
2.4 三者对比速查表
| 功能维度 | setuptools | Poetry | Flit |
|---|---|---|---|
| 虚拟环境管理 | ✗ | ✓ | ✗ |
| 依赖管理 | 需配合工具 | ✓ | ✗(仅声明) |
| lock 文件 | ✗ | ✓ | ✗ |
| C 扩展支持 | ✓ | ✗ | ✗ |
| 打包发布 | ✓ | ✓ | ✓ |
| 配置复杂度 | 高 | 中 | 低 |
| 生态成熟度 | 最高 | 高 | 中 |
| 推荐场景 | 复杂/C扩展 | 日常项目 | 极简小包 |
我的选择建议:
- 新项目、纯 Python → Poetry(首选)
- 有 C/C++ 扩展 → setuptools
- 极简小工具,不需要依赖管理 → Flit
第三章:实战——从零发布一个真实的包
3.1 项目实战:发布一个命令行文件统计工具
我们以 Poetry 为例,完整走遍发布流程。
需求:统计目录下各类型文件的数量和大小,支持命令行调用。
# 初始化项目 poetry new filestats cd filestats
实现核心功能:
# filestats/core.py
from pathlib import Path
from collections import defaultdict
from dataclasses import dataclass
@dataclass
class FileStats:
count: int = 0
total_size: int = 0
@property
def size_mb(self) -> float:
return self.total_size / (1024 * 1024)
def analyze_directory(
path: str | Path,
recursive: bool = True
) -> dict[str, FileStats]:
"""分析目录中各类型文件的统计信息"""
root = Path(path)
if not root.exists():
raise FileNotFoundError(f"路径不存在:{path}")
stats: dict[str, FileStats] = defaultdict(FileStats)
pattern = "**/*" if recursive else "*"
for file_path in root.glob(pattern):
if not file_path.is_file():
continue
suffix = file_path.suffix.lower() or "(无扩展名)"
stats[suffix].count += 1
stats[suffix].total_size += file_path.stat().st_size
return dict(sorted(stats.items(), key=lambda x: x[1].count, reverse=True))
# filestats/cli.py
import click
from pathlib import Path
from .core import analyze_directory
@click.command()
@click.argument("path", default=".", type=click.Path(exists=True))
@click.option("--no-recursive", is_flag=True, help="不递归子目录")
@click.option("--top", default=10, help="显示前 N 种文件类型")
def main(path: str, no_recursive: bool, top: int) -> None:
"""统计目录中各类型文件的数量和大小"""
try:
stats = analyze_directory(path, recursive=not no_recursive)
except FileNotFoundError as e:
click.echo(f"错误:{e}", err=True)
raise click.Abort()
click.echo(f"\n📁 分析路径:{Path(path).resolve()}")
click.echo(f"{'扩展名':<15} {'数量':>8} {'大小(MB)':>12}")
click.echo("-" * 38)
for ext, stat in list(stats.items())[:top]:
click.echo(f"{ext:<15} {stat.count:>8} {stat.size_mb:>11.2f}")
total_files = sum(s.count for s in stats.values())
click.echo(f"\n共计 {total_files} 个文件,{len(stats)} 种类型")
注册 CLI 入口:
[tool.poetry.scripts] filestats = "filestats.cli:main"
本地测试:
poetry install poetry run filestats /path/to/any/directory --top 5
输出示例:
📁 分析路径:/Users/dev/myproject
扩展名 数量 大小(MB)
──────────────────────────────────────
.py 342 1.25
.json 89 0.43
.md 31 0.18
.txt 18 0.08
.yml 12 0.02
共计 492 个文件,5 种类型
3.2 编写测试(发布前必做)
# tests/test_core.py
import pytest
from pathlib import Path
import tempfile
from filestats.core import analyze_directory, FileStats
@pytest.fixture
def temp_dir(tmp_path: Path) -> Path:
"""创建测试用临时目录"""
(tmp_path / "a.py").write_text("print('hello')")
(tmp_path / "b.py").write_text("x = 1")
(tmp_path / "readme.md").write_text("# Test")
subdir = tmp_path / "sub"
subdir.mkdir()
(subdir / "c.py").write_text("pass")
return tmp_path
def test_analyze_directory_counts(temp_dir: Path) -> None:
stats = analyze_directory(temp_dir)
assert stats[".py"].count == 3
assert stats[".md"].count == 1
def test_analyze_directory_non_recursive(temp_dir: Path) -> None:
stats = analyze_directory(temp_dir, recursive=False)
assert stats[".py"].count == 2 # 不含子目录
def test_invalid_path() -> None:
with pytest.raises(FileNotFoundError):
analyze_directory("/nonexistent/path")
poetry run pytest --cov=filestats
第四章:发布到 PyPI 的完整流程
4.1 准备工作
第一步:注册账号
- PyPI 正式环境
- TestPyPI 测试环境(强烈建议先测试)
第二步:配置 API Token(推荐,比密码更安全)
在 PyPI 账户设置中创建 API Token,然后配置 Poetry:
poetry config pypi-token.pypi pypi-AgEIcHlwaS5vcmcXXXXXXXXX poetry config pypi-token.testpypi pypi-AgEIcHlwaS5vcmcYYYYYYYYY
4.2 构建前的 Checklist
发布前检查清单
- 版本号是否已更新?(遵循 SemVer: 主.次.补丁)
- CHANGELOG 或 README 是否已更新?
- 测试全部通过?(pytest)
- 代码格式化?(black, ruff)
- 类型检查通过?(mypy)
- 包名在 PyPI 上是否唯一?
版本号规范(SemVer):
1.0.0
│ │ └── 补丁版本:bug 修复,向后兼容
│ └──── 次版本:新功能,向后兼容
└────── 主版本:破坏性变更
# Poetry 提供便捷的版本管理命令 poetry version patch # 1.0.0 → 1.0.1 poetry version minor # 1.0.0 → 1.1.0 poetry version major # 1.0.0 → 2.0.0
4.3 发布流程
# 步骤一:构建发行包 poetry build # 验证构建结果 ls dist/ # filestats-0.1.0.tar.gz # filestats-0.1.0-py3-none-any.whl # 步骤二:先发布到 TestPyPI 验证 poetry publish --repository testpypi # 步骤三:在新环境中测试安装 pip install --index-url https://test.pypi.org/simple/ filestats filestats --help # 验证功能正常 # 步骤四:确认无误后发布正式版 poetry publish
自动化发布——GitHub Actions:
# .github/workflows/publish.yml
name: Publish to PyPI
on:
push:
tags:
- "v*" # 推送 tag 时触发
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install Poetry
run: pip install poetry
- name: Install dependencies
run: poetry install
- name: Run tests
run: poetry run pytest
- name: Build and publish
env:
POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }}
run: |
poetry build
poetry publish
发布一个新版本只需要:
poetry version patch git add pyproject.toml git commit -m "chore: bump version to $(poetry version -s)" git tag v$(poetry version -s) git push && git push --tags # GitHub Actions 自动完成后续发布
第五章:最佳实践与常见陷阱
5.1 README 是你包的门面
一个好的 README 应该包含:
# filestats
[](https://pypi.org/project/filestats/)
[](https://www.python.org/)
> 快速统计目录中各类型文件的工具
## 安装
\```bash
pip install filestats
\```
## 快速开始
\```bash
# 统计当前目录
filestats .
# 不递归,只显示前 5 种类型
filestats /path/to/dir --no-recursive --top 5
\```
## 作为库使用
\```python
from filestats import analyze_directory
stats = analyze_directory("/path/to/dir")
\```
5.2 常见陷阱与解决方案
陷阱一:包名冲突
在 PyPI 搜索确认名称唯一性。包名用连字符(my-lib),但 Python 导入用下划线(import my_lib),Poetry 会自动处理这个映射。
陷阱二:忘记包含必要文件
# pyproject.toml 中明确包含 [tool.setuptools.package-data] mylib = ["*.json", "*.yaml", "data/*"]
使用 Poetry 时:
[tool.poetry] include = ["filestats/data/*.json"] exclude = ["filestats/tests/*"]
陷阱三:依赖版本过于宽松或过于严格
# ❌ 过于宽松——可能引入不兼容版本 requests = "*" # ❌ 过于严格——妨碍用户使用其他兼容版本 requests = "2.28.1" # ✓ 推荐:允许兼容的次版本更新 requests = "^2.28.0"
第六章:前沿展望
6.1 uv——下一代包管理器
Astral 团队(ruff 的作者)推出的 uv 正在快速崛起。用 Rust 编写,速度比 pip 快 10-100 倍:
# 安装 uv curl -LsSf https://astral.sh/uv/install.sh | sh # 极速安装依赖 uv pip install requests # 比 pip 快 10 倍以上 # 未来:uv 可能统一包管理与项目管理 uv init myproject uv add requests uv run python main.py
6.2 可信发布(Trusted Publishing)
PyPI 正在推广基于 OIDC 的可信发布,无需存储 Token:
# 在 PyPI 项目设置中配置 GitHub Actions 可信发布后 - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 # 不需要任何密钥!OIDC 自动认证
总结
Python 打包生态从混乱走向统一,pyproject.toml 是这场变革的核心。三大工具的选择逻辑清晰:
- Poetry:日常项目的不二之选,一体化体验最佳
- setuptools:复杂需求与 C 扩展的坚实后盾
- Flit:极简小包的轻盈利器
最重要的一步,永远是迈出第一步。选一个你感兴趣的工具,把你手边的项目打个包,发布到 TestPyPI 试试——这个过程远比想象中简单,而那种"我的代码可以被全世界 pip install"的成就感,真的会上瘾。
以上就是Python项目打包与发布的三大工具(setuptools、Poetry、Flit)使用完全指南的详细内容,更多关于Python项目打包与发布的资料请关注脚本之家其它相关文章!
