Chapter 02

项目初始化与 pyproject.toml

理解 PEP 621 标准配置的每一行含义,从此告别 setup.py 和 requirements.txt 的混乱年代

uv init 做了什么?

一句 uv init 看似轻描淡写,它为你生成了一个符合所有现代 Python 标准的项目骨架。

$ uv init myapp
Initialized project `myapp` at `/home/user/myapp`

$ tree myapp
myapp/
├── .python-version      # 固定 Python 版本(默认当前 uv 默认版本)
├── .gitignore           # 包含 .venv, __pycache__ 等
├── README.md            # 空 README
├── main.py              # 入口文件
└── pyproject.toml       # 核心配置

uv init 的四种模式

默认(application)
uv init myapp,创建"应用项目"——不生成 src/ 目录,不配置 build-system,不可发布到 PyPI。适合 Web 后端、CLI 脚本、数据管道等不打算作为库分发的项目。
库模式 --lib
uv init --lib mylib,创建"Python 库"——生成 src/mylib/__init__.py,配置 build-system 为 hatchling,可直接 uv builduv publish 到 PyPI。
打包应用 --package
介于 application 和 lib 之间:生成 src 布局,配置 build-system,但定位为应用(有 console_scripts 入口点)。适合需要 pipx install 或打包成 wheel 发给用户的 CLI 工具。
脚本模式 --script
uv init --script tool.py,生成一个自包含的 PEP 723 单文件脚本,依赖直接写在脚本开头的特殊注释里。第 6 章详细讲。

pyproject.toml 逐段解读

pyproject.toml 是 PEP 518(2016)和 PEP 621(2020)确立的 Python 项目统一配置文件,取代了历史上的 setup.pysetup.cfgPipfilerequirements.txt 等。下面是一个典型的完整示例:

[project]
name = "myapp"
version = "0.1.0"
description = "A modern Python app powered by uv"
readme = "README.md"
requires-python = ">=3.12"
authors = [
  { name = "Alice", email = "alice@example.com" }
]
license = { text = "MIT" }
keywords = ["web", "api"]
classifiers = [
  "Programming Language :: Python :: 3",
  "License :: OSI Approved :: MIT License",
]
dependencies = [
  "fastapi>=0.110",
  "uvicorn[standard]>=0.29",
  "httpx",
]

[project.optional-dependencies]
# 用户可选安装:pip install myapp[postgres]
postgres = ["asyncpg>=0.29"]
redis = ["redis>=5"]

[dependency-groups]
# PEP 735 —— uv 原生支持的开发分组
dev = [
  "pytest>=8",
  "pytest-asyncio",
  "ruff",
  "mypy",
]
docs = ["mkdocs-material"]

[project.scripts]
# 安装后生成 CLI 命令
myapp = "myapp.cli:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.uv]
# uv 专属配置
managed = true
default-groups = ["dev"]

[project] 核心字段详解

字段说明关键点
name项目/包名PyPI 上必须全局唯一;建议用短横线而不是下划线
version版本号遵循 SemVer 或 PEP 440;可通过 dynamic 从代码自动提取
requires-pythonPython 版本约束>=3.12 而不是 ==3.12.*;uv 据此解析兼容依赖
dependencies运行时依赖列表PEP 508 语法:package>=1.0,<2; python_version >= "3.12"
optional-dependencies可选依赖(extras)用户选择安装,如 pip install pkg[all]

dependency-groups:uv 强烈推荐的新方式

传统做法把开发依赖塞进 [project.optional-dependencies].dev,但这在语义上是错的——dev 不是"用户可选安装",而是"开发者专用"。PEP 735 新增的 [dependency-groups] 解决了这个混淆,uv 是第一个原生支持的工具。

[dependency-groups]
dev = ["pytest", "ruff"]
test = ["pytest-cov", "hypothesis"]
docs = ["mkdocs", "mkdocs-material"]

# 分组可以互相包含
ci = [{ include-group = "test" }, "coverage[toml]"]
# 只装默认组(default-groups 配置里的,默认是 dev)
uv sync

# 装特定组
uv sync --group docs

# 生产部署:完全不装开发组
uv sync --no-dev
# 或
uv sync --only-group main
extras vs dependency-groups 选择原则

extras(optional-dependencies):面向"下载你的包的人",让他们选择启用什么功能(如 mypkg[postgres] 附带 asyncpg)。
dependency-groups:面向"开发者自己",定义 dev/test/docs/lint 等本地工作流依赖。不会出现在 wheel 里,不影响下游用户。

[tool.uv] — uv 专属配置

虽然大多数字段都是 PEP 标准,uv 也有一些扩展配置放在 [tool.uv] 里:

[tool.uv]
# 启用项目模式(默认 true,关闭则退化为 pip 兼容模式)
managed = true

# 默认同步哪些依赖组
default-groups = ["dev"]

# 依赖覆盖:强制使用特定版本(绕过子依赖声明)
override-dependencies = ["urllib3<2"]

# 约束:不强制安装,但若安装则必须满足
constraint-dependencies = ["setuptools>=68"]

# 对不完整元数据包的回退策略
resolution = "highest"  # highest / lowest / lowest-direct

# 在项目中自动管理的 Python 版本
python-preference = "managed"  # managed / system / only-managed / only-system

# 可编辑安装(开发模式)
[tool.uv.sources]
# 把某个依赖指向本地路径/git/workspace
mylib = { path = "../mylib", editable = true }
fastapi = { git = "https://github.com/tiangolo/fastapi", rev = "master" }

[build-system] — 构建后端选择

如果你的项目要打包发布(uv build),必须配置 build-system。uv 本身不是构建后端,只是调用你选的后端。

hatchling(推荐)
Hatch 项目的构建后端,极简配置,纯 Python 项目的首选。requires = ["hatchling"]build-backend = "hatchling.build"。uv init --lib 的默认选择。
setuptools
最老的构建后端,仍广泛使用。配合 C 扩展、复杂打包需求时有用。build-backend = "setuptools.build_meta"
maturin
用于 Rust + Python 混合项目(如 pydantic-core、polars)。build-backend = "maturin",自动编译 Rust 代码生成 wheel。
scikit-build-core
C/C++ CMake 项目的现代构建后端,取代老旧的 scikit-build。适合绑定 C++ 库。
uv_build(实验性)
uv 自己的 Rust 实现后端,目前仅支持纯 Python 项目,优势是构建速度极快。未来可能成为 uv init --lib 的默认选项。

目录布局:flat vs src

flat 布局(application 默认)

myapp/
├── pyproject.toml
├── main.py
└── myapp/
    ├── __init__.py
    └── ...

src 布局(library 默认)

mylib/
├── pyproject.toml
└── src/
    └── mylib/
        ├── __init__.py
        └── ...
为什么 src 布局被库项目推荐?

flat 布局下 import mylib 可能隐式导入了项目根目录的 mylib/,即使你没装包。src 布局强制你必须安装(哪怕 pip install -e .),避免"本地能跑、装完用户跑不起来"的幽灵 bug。库项目一定用 src。

[project.scripts] — 生成 CLI 命令

[project.scripts]
myapp = "myapp.cli:main"
myapp-admin = "myapp.admin:cli"

[project.gui-scripts]  # Windows 下不开控制台窗口
myapp-gui = "myapp.gui:run"

[project.entry-points."myapp.plugins"]  # 插件系统
csv = "myapp.plugins.csv_backend:Plugin"

安装项目后(uv sync),这些命令会自动出现在 venv 的 .venv/bin/ 下。uv run myapp 即可直接调用。

动态字段(dynamic)

有时候你希望 version、readme 等字段从代码/文件自动读取,而不是硬编码。用 dynamic 声明:

[project]
name = "myapp"
dynamic = ["version"]

[tool.hatch.version]
path = "src/myapp/__init__.py"  # 从代码读 __version__
本章小结

pyproject.toml 是现代 Python 项目的单一事实源,按 PEP 621 标准组织。记住三个分区:[project] 放通用元数据和运行时依赖;[dependency-groups](PEP 735)放开发者专属分组;[tool.uv] 放 uv 扩展配置。库项目用 src 布局 + hatchling,应用项目保持 flat 即可。下一章深入 uv add/uv remove/uv sync 的日常工作流。