使用 uv 构建并发布 Python 包到 PyPI

使用 uv 构建并发布 Python 包到 PyPI 完整教程

目录

  1. 概述
  2. 环境准备
  3. 创建项目
  4. pyproject.toml 配置详解
  5. 编写包代码
  6. 构建包
  7. 发布到 PyPI
  8. 使用 TestPyPI 测试
  9. 版本管理
  10. 最佳实践
  11. 常见问题

概述

本教程将指导你使用 uv(一个超快的 Python 包管理器)来:

  • 创建一个标准的 Python 包项目
  • 配置 pyproject.toml
  • 构建源码分发包(sdist)和 wheel 包
  • 发布到 PyPI(Python Package Index)

为什么选择 uv?

  • 极快:比 pip 快 10-100 倍
  • 一体化:替代 pip、pip-tools、poetry、pyenv、twine 等多个工具
  • 现代:原生支持 pyproject.toml 和最新的 Python 打包标准
  • 可靠:提供锁文件和可重现的环境

环境准备

1. 安装 uv

macOS 和 Linux:

1
curl -LsSf https://astral.sh/uv/install.sh | sh

Windows (PowerShell):

1
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

通过 pip 安装:

1
pip install uv

2. 验证安装

1
uv --version

3. 安装 Python(如果需要)

1
2
3
4
5
# 安装特定版本的 Python
uv python install 3.12

# 查看已安装的 Python 版本
uv python list

创建项目

方法一:使用 uv init 自动创建

1
2
3
4
5
6
7
# 创建新项目
uv init my-awesome-package

# 进入项目目录
cd my-awesome-package

# 项目结构会自动创建

方法二:手动创建项目结构

my-awesome-package/
├── src/
│   └── my_awesome_package/
│       ├── __init__.py
│       └── core.py
├── tests/
│   ├── __init__.py
│   └── test_core.py
├── pyproject.toml
├── README.md
├── LICENSE
└── .gitignore

创建目录结构:

1
2
3
mkdir -p my-awesome-package/src/my_awesome_package
mkdir -p my-awesome-package/tests
cd my-awesome-package

pyproject.toml 配置详解

pyproject.toml 是 Python 项目的核心配置文件,包含三个主要部分:

完整的 pyproject.toml 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
[build-system]
requires = ["hatchling >= 1.26"]
build-backend = "hatchling.build"

[project]
name = "my-awesome-package"
version = "0.1.0"
authors = [
{ name = "Your Name", email = "your.email@example.com" }
]
maintainers = [
{ name = "Your Name", email = "your.email@example.com" }
]
description = "A short description of your package"
readme = "README.md"
requires-python = ">=3.9"
license = "MIT"
license-files = ["LICEN[CS]E*"]
keywords = ["example", "package", "tutorial"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]

dependencies = [
"requests>=2.28.0",
"pydantic>=2.0.0",
]

[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"ruff>=0.1.0",
"mypy>=1.0.0",
]
docs = [
"mkdocs>=1.5.0",
"mkdocs-material>=9.0.0",
]

[project.urls]
Homepage = "https://github.com/yourusername/my-awesome-package"
Documentation = "https://github.com/yourusername/my-awesome-package#readme"
Repository = "https://github.com/yourusername/my-awesome-package"
Issues = "https://github.com/yourusername/my-awesome-package/issues"
Changelog = "https://github.com/yourusername/my-awesome-package/blob/main/CHANGELOG.md"

[project.scripts]
my-cli = "my_awesome_package.cli:main"

[project.gui-scripts]
my-gui = "my_awesome_package.gui:main"

[tool.hatch.build.targets.wheel]
packages = ["src/my_awesome_package"]

[tool.ruff]
line-length = 88
target-version = "py39"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP"]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --cov=my_awesome_package"

[tool.mypy]
python_version = "3.9"
warn_return_any = true
warn_unused_configs = true

各部分详解

[build-system] - 构建系统配置

1
2
3
[build-system]
requires = ["hatchling >= 1.26"]
build-backend = "hatchling.build"

常用的构建后端:

构建后端 requires build-backend
Hatchling (推荐) "hatchling >= 1.26" "hatchling.build"
Setuptools "setuptools >= 77.0.3" "setuptools.build_meta"
Flit "flit_core >= 3.12.0, <4" "flit_core.buildapi"
PDM "pdm-backend >= 2.4.0" "pdm.backend"
uv-build "uv_build >= 0.11.7, <0.12.0" "uv_build"

[project] - 项目元数据

字段 说明 示例
name 包名(PyPI 上的名称) "my-awesome-package"
version 版本号 "0.1.0"
description 简短描述 "A short description"
readme README 文件路径 "README.md"
requires-python Python 版本要求 ">=3.9"
license SPDX 许可证表达式 "MIT"
dependencies 运行时依赖 ["requests>=2.28.0"]

[project.urls] - 项目链接

1
2
3
4
5
[project.urls]
Homepage = "https://github.com/user/repo"
Documentation = "https://readthedocs.org"
Repository = "https://github.com/user/repo"
Issues = "https://github.com/user/repo/issues"

[project.scripts] - 命令行工具

1
2
[project.scripts]
my-cli = "my_package.module:function"

安装后会创建一个 my-cli 命令。

[project.optional-dependencies] - 可选依赖

1
2
3
[project.optional-dependencies]
dev = ["pytest", "ruff"]
docs = ["mkdocs"]

安装时使用:pip install my-package[dev]


编写包代码

1. 创建 init.py

src/my_awesome_package/init.py:

1
2
3
4
5
6
7
8
9
"""My Awesome Package - A short description."""

__version__ = "0.1.0"
__author__ = "Your Name"
__email__ = "your.email@example.com"

from .core import hello, add_numbers

__all__ = ["hello", "add_numbers", "__version__"]

2. 编写核心模块

src/my_awesome_package/core.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
"""Core functionality of my awesome package."""


def hello(name: str = "World") -> str:
"""Say hello to someone.

Args:
name: The name to greet. Defaults to "World".

Returns:
A greeting string.
"""
return f"Hello, {name}!"


def add_numbers(a: int | float, b: int | float) -> int | float:
"""Add two numbers together.

Args:
a: First number.
b: Second number.

Returns:
The sum of a and b.
"""
return a + b


class Calculator:
"""A simple calculator class."""

def __init__(self) -> None:
self.history: list[str] = []

def add(self, a: float, b: float) -> float:
"""Add two numbers."""
result = a + b
self.history.append(f"{a} + {b} = {result}")
return result

def subtract(self, a: float, b: float) -> float:
"""Subtract b from a."""
result = a - b
self.history.append(f"{a} - {b} = {result}")
return result

def get_history(self) -> list[str]:
"""Get calculation history."""
return self.history.copy()

3. 创建 CLI 入口(可选)

src/my_awesome_package/cli.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
"""Command-line interface for my awesome package."""

import argparse
import sys

from . import __version__
from .core import hello


def main() -> int:
"""Main entry point for the CLI."""
parser = argparse.ArgumentParser(
description="My Awesome Package CLI"
)
parser.add_argument(
"--version",
action="version",
version=f"%(prog)s {__version__}"
)
parser.add_argument(
"--name",
default="World",
help="Name to greet"
)

args = parser.parse_args()
print(hello(args.name))
return 0


if __name__ == "__main__":
sys.exit(main())

4. 编写测试

tests/test_core.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
"""Tests for core module."""

import pytest

from my_awesome_package.core import Calculator, add_numbers, hello


class TestHello:
"""Tests for hello function."""

def test_hello_default(self):
assert hello() == "Hello, World!"

def test_hello_with_name(self):
assert hello("Alice") == "Hello, Alice!"

def test_hello_empty_string(self):
assert hello("") == "Hello, !"


class TestAddNumbers:
"""Tests for add_numbers function."""

def test_add_positive_integers(self):
assert add_numbers(2, 3) == 5

def test_add_negative_integers(self):
assert add_numbers(-1, -1) == -2

def test_add_floats(self):
assert add_numbers(1.5, 2.5) == 4.0

def test_add_mixed(self):
assert add_numbers(1, 2.5) == 3.5


class TestCalculator:
"""Tests for Calculator class."""

def test_add(self):
calc = Calculator()
assert calc.add(2, 3) == 5

def test_subtract(self):
calc = Calculator()
assert calc.subtract(5, 3) == 2

def test_history(self):
calc = Calculator()
calc.add(1, 2)
calc.subtract(5, 3)
history = calc.get_history()
assert len(history) == 2
assert "1 + 2 = 3" in history[0]

5. 创建 README.md

1
2
3
4
5
6
7
8
9
10
11
# My Awesome Package

[![PyPI version](https://badge.fury.io/py/my-awesome-package.svg)](https://pypi.org/project/my-awesome-package/)
[![Python Versions](https://img.shields.io/pypi/pyversions/my-awesome-package.svg)](https://pypi.org/project/my-awesome-package/)

A short description of your package.

## Installation

```bash
pip install my-awesome-package

Quick Start

1
2
3
4
5
6
7
8
from my_awesome_package import hello, add_numbers

# Say hello
print(hello("World")) # Output: Hello, World!

# Add numbers
result = add_numbers(2, 3)
print(result) # Output: 5

CLI Usage

1
2
my-cli --name "Alice"
# Output: Hello, Alice!

Development

1
2
3
4
5
6
7
8
9
10
11
12
# Clone the repository
git clone https://github.com/yourusername/my-awesome-package.git
cd my-awesome-package

# Install dependencies
uv sync --extra dev

# Run tests
uv run pytest

# Run linter
uv run ruff check .

License

MIT License - see LICENSE for details.

### 6. 创建 LICENSE

MIT License

Copyright (c) 2024 Your Name

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

---

## 构建包

### 1. 使用 uv 管理依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    # 添加运行时依赖
uv add requests pydantic

# 添加开发依赖
uv add --dev pytest ruff mypy

# 同步依赖(安装所有依赖)
uv sync

# 只安装特定可选依赖
uv sync --extra dev

### 2. 运行测试

``` bash
# 运行测试
uv run pytest

# 运行测试并生成覆盖率报告
uv run pytest --cov=my_awesome_package --cov-report=html

3. 代码检查

1
2
3
4
5
6
7
8
# 运行 linter
uv run ruff check .

# 运行 formatter
uv run ruff format .

# 运行类型检查
uv run mypy src/

4. 构建分发包

1
2
3
4
5
# 构建 sdist 和 wheel
uv build

# 构建时不使用本地源码覆盖(推荐用于发布)
uv build --no-sources

构建完成后,会在 dist/ 目录下生成:

dist/
├── my_awesome_package-0.1.0.tar.gz        # 源码分发包 (sdist)
└── my_awesome_package-0.1.0-py3-none-any.whl  # wheel 包

5. 验证构建

1
2
3
4
5
6
# 检查包的元数据
uv pip show --files dist/my_awesome_package-0.1.0-py3-none-any.whl

# 使用 twine 检查(可选)
pip install twine
twine check dist/*

发布到 PyPI

1. 注册 PyPI 账号

  1. 访问 https://pypi.org/account/register/
  2. 完成注册并验证邮箱
  3. 启用双因素认证(推荐)

2. 生成 API Token

  1. 登录 PyPI
  2. 访问 https://pypi.org/manage/account/token/
  3. 点击 “Add API token”
  4. 设置 Token 名称和权限范围
  5. 立即复制并保存 Token(只显示一次)

Token 格式:pypi-AgEI...

3. 配置认证信息

方法一:环境变量(推荐)

1
export UV_PUBLISH_TOKEN="pypi-your-token-here"

方法二:命令行参数

1
uv publish --token "pypi-your-token-here"

方法三:使用 .pypirc 文件

创建 ~/.pypirc

1
2
3
4
5
6
7
[pypi]
username = __token__
password = pypi-your-token-here

[testpypi]
username = __token__
password = pypi-your-testpypi-token-here

设置文件权限:

1
chmod 600 ~/.pypirc

4. 发布到 PyPI

1
2
3
4
5
6
7
8
# 发布所有构建产物
uv publish

# 使用 Token 发布
uv publish --token "pypi-your-token-here"

# 发布到指定索引(需要在 pyproject.toml 中配置)
uv publish --index pypi

5. 验证发布

发布成功后,访问 https://pypi.org/project/my-awesome-package/ 查看你的包。

安装测试:

1
2
# 在新环境中测试安装
uv run --with my-awesome-package --no-project -- python -c "from my_awesome_package import hello; print(hello())"

使用 TestPyPI 测试

在正式发布前,建议先在 TestPyPI 上测试。

1. 注册 TestPyPI 账号

访问 https://test.pypi.org/account/register/

2. 配置 TestPyPI 索引

pyproject.toml 中添加:

1
2
3
4
5
[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true

3. 发布到 TestPyPI

1
2
# 生成 TestPyPI Token 后
uv publish --index testpypi --token "pypi-your-testpypi-token"

4. 从 TestPyPI 安装测试

1
2
3
4
5
6
7
8
9
10
# 创建测试环境
uv venv test-env
source test-env/bin/activate # Linux/macOS
# test-env\Scripts\activate # Windows

# 从 TestPyPI 安装
uv pip install --index-url https://test.pypi.org/simple/ my-awesome-package

# 测试导入
python -c "from my_awesome_package import hello; print(hello())"

版本管理

使用 uv version 管理版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 查看当前版本
uv version

# 设置精确版本
uv version 1.0.0

# 预览版本变更(不实际修改)
uv version 2.0.0 --dry-run

# 语义化版本升级
uv version --bump patch # 0.1.0 -> 0.1.1
uv version --bump minor # 0.1.0 -> 0.2.0
uv version --bump major # 0.1.0 -> 1.0.0

# 预发布版本
uv version --bump patch --bump beta # 0.1.0 -> 0.1.1b1
uv version --bump major --bump alpha # 0.1.0 -> 1.0.0a1

# 从预发布升级到稳定版
uv version --bump stable # 1.0.0b2 -> 1.0.0

版本号规范

遵循 语义化版本

  • MAJOR.MINOR.PATCH (例如:1.2.3)
  • MAJOR: 不兼容的 API 变更
  • MINOR: 向下兼容的功能性新增
  • PATCH: 向下兼容的问题修正

预发布版本:1.0.0a1, 1.0.0b2, 1.0.0rc1


最佳实践

1. 项目结构

推荐使用 src 布局:

my-package/
├── src/
│   └── my_package/
│       ├── __init__.py
│       ├── core.py
│       └── py.typed        # PEP 561 类型标记
├── tests/
├── pyproject.toml
├── README.md
├── LICENSE
└── .gitignore

2. 依赖管理

1
2
3
4
5
6
7
8
9
# 明确指定版本范围
dependencies = [
"requests>=2.28.0,<3.0.0",
"pydantic>=2.0.0",
]

# 分离开发依赖
[project.optional-dependencies]
dev = ["pytest>=7.0", "ruff>=0.1.0"]

3. CI/CD 自动发布

GitHub Actions 示例 (.github/workflows/publish.yml):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
name: Publish to PyPI

on:
release:
types: [published]

jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write # 用于可信发布

steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v4

- name: Set up Python
run: uv python install 3.12

- name: Run tests
run: |
uv sync --extra dev
uv run pytest

- name: Build package
run: uv build --no-sources

- name: Publish to PyPI
run: uv publish
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}

4. 可信发布(Trusted Publishing)

PyPI 支持无需 Token 的可信发布:

  1. 在 PyPI 项目设置中添加 GitHub Actions 作为可信发布者
  2. 配置 GitHub Actions 使用 id-token: write 权限
  3. 不需要设置任何 Token
1
2
3
- name: Publish to PyPI
run: uv publish
# 不需要设置 UV_PUBLISH_TOKEN

5. 发布检查清单

发布前确认:

  • 更新版本号
  • 更新 CHANGELOG
  • 运行测试并确保通过
  • 运行 linter 和类型检查
  • 更新 README(如有必要)
  • 构建并验证包
  • 在 TestPyPI 上测试
  • 确认许可证文件
1
2
3
4
5
6
# 完整的发布流程
uv run pytest && \
uv run ruff check . && \
uv build --no-sources && \
twine check dist/* && \
uv publish

常见问题

Q1: 包名已被占用怎么办?

  • 使用更有创意的名称
  • 添加前缀(如 yourname-packagename
  • 检查 PyPI 上是否真的需要该名称

Q2: 构建失败怎么办?

1
2
3
4
5
6
7
8
# 清理构建缓存
rm -rf dist/ build/ *.egg-info

# 重新构建
uv build --no-sources

# 查看详细错误
uv build --verbose

Q3: 如何删除已发布的版本?

PyPI 不允许删除已发布的版本,只能:

  • 发布新版本
  • 使用 yanking 标记版本为不推荐

Q4: 如何处理二进制扩展?

如果包含 C/C++ 扩展,需要使用 cibuildwheel

1
2
3
# GitHub Actions
- name: Build wheels
uses: pypa/cibuildwheel@v2.16

Q5: 发布后如何更新包?

1
2
3
4
5
6
# 更新版本号
uv version --bump patch

# 重新构建并发布
uv build --no-sources
uv publish

Q6: 如何在私有仓库发布?

1
2
3
4
5
# pyproject.toml
[[tool.uv.index]]
name = "private"
url = "https://private.pypi.org/simple/"
publish-url = "https://private.pypi.org/legacy/"
1
uv publish --index private

参考资源


快速参考命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 项目初始化
uv init my-package
cd my-package

# 依赖管理
uv add requests
uv add --dev pytest
uv sync

# 运行代码
uv run python -m my_package
uv run pytest

# 版本管理
uv version
uv version --bump patch

# 构建和发布
uv build --no-sources
uv publish

# 发布到 TestPyPI
uv publish --index testpypi

最后更新:2024


使用 uv 构建并发布 Python 包到 PyPI
https://tingfeng347.github.io/2026/04/28/使用 uv 构建并发布 Python 包到 PyPI/
作者
Tingfeng
发布于
2026年4月28日
许可协议