1. 职责
pyproject.toml 是现代 Python 项目的中心配置文件。uv 会用它识别项目根目录和依赖,构建工具会从中读取打包信息,FastAPI CLI、Ruff、pytest 等工具也可以在各自的配置区读取设置。
这并不意味着所有内容都属于同一种规范。一个 pyproject.toml 通常会包含下面几类配置:
| 区域 | 谁定义 | 用途 |
|---|---|---|
[build-system] | Python 构建规范 | 声明用什么工具构建项目 |
[project] | Python 项目元数据规范 | 声明名称、版本、Python 版本和依赖等信息 |
[dependency-groups] | Python 依赖组规范 | 声明测试、格式化等开发依赖组 |
[tool.*] | 每个工具自己定义 | 保存 uv、FastAPI、Ruff、pytest 等工具配置 |
本章重点介绍项目元数据、构建系统和工具配置。[dependency-groups] 会在下一章结合依赖管理详细说明。
例如,当前 fastapi-langchain 项目的核心内容如下:
01[project]02name = "fastapi-langchain"03version = "0.1.0"04description = "FastAPI service for LangChain and LangGraph examples"05readme = "README.md"06authors = [07{ name = "yangbo", email = "yangbo@example.com" },08]09requires-python = ">=3.13"10dependencies = [11"fastapi[standard]>=0.141.1",12"langchain>=1.3.16",13"langchain-openai>=1.6.0",14"pydantic-settings>=2.10.1",15]1617[tool.fastapi]18entrypoint = "app.main:app"
[project] 遵循标准项目元数据规范。uv 可以直接用它管理项目;pip 安装源码项目时,则会通过构建后端获取这些元数据。[tool.fastapi] 由 FastAPI CLI 解释,Python 本身不会因为看到它就启动服务。
项目运行时的模型温度、数据库连接池大小和 CORS 来源可以放进独立的应用配置,但不建议全部塞进 pyproject.toml。pyproject.toml 更适合描述「这个项目是什么、依赖什么、开发工具如何工作」。
2. 项目字段
[project] 中最常见的字段如下:
| 字段 | 含义 | 当前项目示例 |
|---|---|---|
name | 项目或分发包名称 | fastapi-langchain |
version | 项目版本 | 0.1.0 |
description | 一句话说明 | FastAPI 与 LangChain 案例服务 |
readme | 项目说明文件 | README.md |
requires-python | 支持的 Python 版本范围 | >=3.13 |
dependencies | 运行项目所需依赖 | FastAPI、LangChain 等 |
authors | 作者信息 | 姓名和邮箱 |
license | SPDX 许可证表达式 | MIT、Apache-2.0 |
license-files | 随分发包包含的许可证文件 | LICENSE* |
keywords | 用于检索的关键词 | fastapi、langchain |
classifiers | PyPI 分类信息 | Python 版本、开发状态等 |
name 是项目标识,不一定与 Python 导入名完全相同。项目名常使用连字符:
1[project]2name = "fastapi-langchain"
Python 包目录通常使用下划线,或者像当前项目一样直接使用 app:
1from app.factory import create_app
requires-python 不是「推荐版本」,而是项目兼容范围。uv 会根据它选择可用依赖;没有显式配置目标版本时,Ruff 也可以据此推断应允许哪些 Python 语法:
1[project]2requires-python = ">=3.13"
如果项目代码使用了 Python 3.13 才具备的能力,就不应写成 >=3.10。反过来,如果服务需要在 Python 3.11 环境部署,也不能只因为开发电脑装了 3.13 就把最低版本抬高。
version 描述项目发布版本,并应符合 PEP 440,例如 0.1.0、1.0.0rc1。内部 Web 应用即使不发布到 PyPI,也可以使用版本号标识迭代阶段,并把它传给 FastAPI 的 OpenAPI 信息。版本号不等于 Git 提交号,也不等于依赖锁文件版本。
3. 元数据
作者、许可证、链接等字段不会改变 FastAPI 的运行逻辑,但它们让项目更容易识别和维护:
01[project]02name = "fastapi-langchain"03version = "0.1.0"04description = "FastAPI service for LangChain and LangGraph examples"05readme = "README.md"06requires-python = ">=3.13"07license = "MIT"08license-files = ["LICENSE*"]09keywords = ["fastapi", "langchain", "langgraph"]10authors = [11{ name = "yangbo", email = "yangbo@example.com" },12]1314[project.urls]15Repository = "https://github.com/yangbo5207/fastapi-langchain"16Documentation = "https://langchainpy.usehook.cn"
readme 指向的文件必须存在。项目准备构建成分发包时,错误路径会导致构建失败。
license 使用 SPDX 表达式描述授权方式。license-files 用 glob 模式声明需要放进分发包的许可证文件;准备构建项目时,应确保至少有一个实际文件与模式匹配。旧项目中可能还能看到许可证表或 License :: classifier,新项目不建议继续使用这些已弃用的写法。
authors 是由内联表组成的数组。每位作者可以提供 name、email 或同时提供两者,但至少要填写其中一项。
classifiers 是一组固定分类字符串,主要用于发布到 PyPI。普通内部 FastAPI 服务没有必要为了看起来完整而堆很多分类字段。配置应服务于实际用途,而不是追求字段数量。
有些工具支持从 Git 标签或其他文件动态计算版本。标准 [project] 通过 dynamic 声明哪些字段由构建后端提供:
1[project]2name = "fastapi-langchain"3dynamic = ["version"]
如果没有配置对应构建工具,就不要使用 dynamic。初学项目直接写明 version = "0.1.0" 更容易理解。
4. 构建系统
[build-system] 告诉 Python 工具:要把当前项目构建或安装成一个包,应先安装什么构建后端,再调用哪个后端。
使用 uv 的构建后端时,可以写成:
1[build-system]2requires = ["uv_build>=0.12.5,<0.13"]3build-backend = "uv_build"
这两个字段的职责不同:
requires是执行构建所需的依赖;build-backend是实际接收构建调用的 Python 对象。
什么是 wheel
wheel 是 Python 的一种已构建分发格式,文件扩展名是 .whl。它通常包含可以直接安装的 Python 代码、包元数据和资源文件。安装 wheel 时,安装工具主要负责解包并把文件放到正确位置,不需要再调用当前项目的构建后端从源码生成安装产物,因此通常比安装源码分发包更快、更稳定。
一个纯 Python wheel 的文件名可能是:
1fastapi_langchain-0.1.0-py3-none-any.whl
文件名中的标签描述兼容范围:py3 表示支持 Python 3,none 表示不依赖特定 Python ABI,any 表示不绑定操作系统或 CPU 平台。包含 C、Rust 等本地扩展的 wheel 会带有更具体的 Python、ABI 和平台标签,不能在任意环境中通用。
wheel 与源码分发包的区别在于:wheel 已经完成构建,而源码分发包通常是 .tar.gz,安装时还需要在目标环境中调用构建后端。执行下面的命令时,构建工具一般会把两种产物放进 dist/:
1uv build
wheel 也不是独立可执行程序或 Docker 镜像。它通常不包含 Python 解释器、操作系统依赖和所有第三方依赖;项目的第三方依赖仍记录在元数据中,由 uv、pip 等安装工具继续解析和安装。
Web 服务能够通过 uv run fastapi dev 启动,不代表它一定需要构建成 wheel。如果刻意采用不安装自身包的扁平应用结构,可以使用 uv init --no-package 创建项目,此时可以不声明 [build-system]。从 uv 0.12 开始,直接执行 uv init 默认创建可打包应用,并生成 src 布局和构建系统;不要把这两种项目模式混为一谈。
下面这些场景通常需要明确构建系统:
- 项目需要安装自身包,并采用标准
src布局; - 要发布 wheel 或源码包;
- 要使用
[project.scripts]生成可安装命令; - 其他项目需要把它作为依赖安装。
uv_build 适合构建纯 Python 包。如果项目包含需要编译的本地扩展,应根据所用语言选择 Maturin、scikit-build-core 等相应的构建后端。
不要把 [build-system] 误解成 Docker 或生产部署配置。它负责 Python 包构建,不负责启动 Uvicorn、创建数据库或设置云平台环境变量。
5. 入口
Python 标准项目元数据可以通过 [project.scripts] 声明安装后可用的命令入口。使用这个区域前,项目必须声明构建系统,并采用可以被构建后端安装的包结构:
1[build-system]2requires = ["uv_build>=0.12.5,<0.13"]3build-backend = "uv_build"45[project.scripts]6inspect-api = "app.cli:inspect_application"
右侧的 app.cli:inspect_application 表示:导入 app/cli.py,调用其中的 inspect_application 函数。对应代码可以是:
1from app.main import app234def inspect_application() -> None:5print(f"{app.title} {app.version}")
然后可以执行:
1uv run inspect-api
这里必须注意:[project.scripts] 不是 package.json 的 scripts。它不能直接写成下面这样:
1# 错误理解:右侧不是任意 Shell 命令2[project.scripts]3dev = "fastapi dev"
右侧必须是 模块路径:可调用对象。[project.scripts] 适合暴露项目自身提供的 CLI,而不是单纯给 Shell 命令起别名。如果只想缩短开发命令,可以使用 Makefile、Just 或其他任务运行器。
FastAPI CLI 自己还支持另一种入口配置:
1[tool.fastapi]2entrypoint = "app.main:app"
它不是新建一个 uv run dev 命令,而是告诉 fastapi dev 和 fastapi run 应该去哪里找到 FastAPI 应用:
1uv run fastapi dev
当前项目采用的正是这种方式。
6. 工具区
[tool.*] 是 pyproject.toml 最灵活的区域。点号后面的名称代表工具自己的命名空间:
1[tool.fastapi]2entrypoint = "app.main:app"34[tool.ruff]5line-length = 10067[tool.pytest.ini_options]8testpaths = ["tests"]
这里的层级关系是:
1{2"tool": {3"fastapi": {"entrypoint": "app.main:app"},4"ruff": {"line-length": 100},5"pytest": {6"ini_options": {"testpaths": ["tests"]},7},8},9}
不同工具的字段不能互换。line-length 是 Ruff 理解的字段,放进 [tool.fastapi] 不会让 FastAPI 自动限制代码行宽。
工具一般只读取自己的区域,因此同一个文件可以集中保存多个工具的设置。集中配置的好处是项目入口清晰;如果配置非常庞大,部分工具也支持独立的 TOML 文件,例如 Ruff 可以使用 ruff.toml。
选择集中还是拆分时,可以采用简单原则:常用且较短的配置放在 pyproject.toml;规模很大、需要独立继承或由不同团队维护的配置再拆出去。
配置边界
pyproject.toml 可以承载很多内容,但仍有明确边界:
| 内容 | 是否适合 | 原因 |
|---|---|---|
| Python 版本和依赖 | 适合 | 属于项目定义 |
| FastAPI 应用入口 | 适合 | FastAPI CLI 官方工具配置 |
| Ruff、pytest 配置 | 适合 | 开发工具配置 |
| 默认模型名称 | 视情况 | 更适合独立应用配置,除非仅用于开发工具 |
| 数据库密码 | 不适合 | 会进入版本控制 |
| DeepSeek API Key | 不适合 | 属于敏感运行环境配置 |
| 用户每次请求的温度 | 不适合 | 属于动态业务输入 |
还要区分 pyproject.toml 与 uv.lock:前者声明允许的依赖范围,后者记录 uv 实际解析出的精确版本。开发者会维护前者,uv 负责生成后者。
当项目出现问题时,可以先判断是哪一层:
uv sync失败,优先检查项目依赖与 uv 配置;fastapi dev找不到应用,检查[tool.fastapi];- Ruff 规则不生效,检查
[tool.ruff]; - 服务启动后模型配置错误,检查应用 Settings 和环境变量,而不是盲目修改
[project]。