1. TOML
Python 项目里经常会出现 pyproject.toml、ruff.toml、config.toml。它们的文件名不同,但都使用 TOML 语法。
TOML 是一种配置文件格式。它的目标把一组有相对比较复杂的数据保存下来,让程序或开发工具读取。例如,一个 FastAPI 服务可能需要这些配置:
1[app]2name = "FastAPI LangChain"3debug = true45[server]6host = "127.0.0.1"7port = 8000
从人的视角看,这份文件分成了应用和服务器两组配置;从 Python 的视角看,它最终会变成一层层字典:
01{02"app": {03"name": "FastAPI LangChain",04"debug": True,05},06"server": {07"host": "127.0.0.1",08"port": 8000,09},10}
因此,学习 TOML 时可以始终抓住一个核心:TOML 正在描述一棵由键、值、字典和列表组成的数据结构。
2. 读取
首先,我们要了解的是项目是如何读取 .toml 配置文件的
| 文件或区域 | 读取者 | 是否需要应用自己接入 |
|---|---|---|
pyproject.toml 中的 [project] | uv、构建工具等 | 不需要,运行对应工具时读取 |
pyproject.toml 中的 [tool.fastapi] | FastAPI CLI | 不需要,执行 fastapi dev 或 fastapi run 时读取 |
自定义 config.toml | 没有默认读取者 | 需要使用 tomllib 或配置库显式接入 |
默认的 fastapi 项目中,有一个叫做 pyproject.toml 的配置文件:
1[tool.fastapi]2entrypoint = "app.main:app"
当我们执行下面的命令时,FastAPI CLI 会读取这段配置,找到 app/main.py 导出的 app 对象:
1uv run fastapi dev
至于 .env 配置,我们之前已经在 app/core/config.py 中接入了 .env:
01from functools import lru_cache0203from pydantic_settings import BaseSettings, SettingsConfigDict040506class Settings(BaseSettings):07model_config = SettingsConfigDict(08env_file=".env",09env_file_encoding="utf-8",10extra="ignore",11)1213app_name: str = "FastAPI LangChain"14deepseek_model: str = "deepseek-v4-flash"151617@lru_cache18def get_settings() -> Settings:19return Settings()
这里不是 FastAPI 自动读取 .env,而是创建 Settings() 时,Pydantic Settings 根据 env_file 读取文件并校验字段。@lru_cache 让同一进程复用配置对象,避免每个请求都重新读取。
如果我们新增一个位于项目根目录的 config.toml,最直接的接入方式是标准库 tomllib:
01from pathlib import Path02from typing import Any03import tomllib0405PROJECT_ROOT = Path(__file__).resolve().parents[2]06CONFIG_PATH = PROJECT_ROOT / "config.toml"070809def load_config() -> dict[str, Any]:10with CONFIG_PATH.open("rb") as file:11return tomllib.load(file)
然后在创建应用时主动调用它:
01from fastapi import FastAPI0203from app.core.config_loader import load_config0405config = load_config()06app_config = config["app"]0708app = FastAPI(09title=app_config["name"],10debug=app_config["debug"],11)
这段代码能说明读取过程,但直接操作多层字典不适合大型项目。后面的“应用配置”和“环境”两篇会把 TOML 接入 Pydantic Settings:由 TomlConfigSettingsSource 负责读取,再由 Pydantic 校验字段和类型。它仍然需要我们显式配置,只是不必自己维护解析和类型转换逻辑。
tomllib 从 Python 3.11 开始进入标准库。当前项目使用 Python 3.13,可以直接导入,无需安装依赖。Python 3.13 的 tomllib 解析 TOML 1.0.0,读取文件时必须使用二进制模式 rb。仍在使用 Python 3.10 的项目可以安装兼容包 tomli,但当前项目不需要。
读取已经在内存中的 TOML 字符串时,使用 loads():
1import tomllib23content = """4[server]5port = 80006"""78config = tomllib.loads(content)9print(config["server"]["port"])
load() 接收文件对象,loads() 接收字符串,这与 json.load() 和 json.loads() 的区别相似。
解析失败时,tomllib 会抛出 TOMLDecodeError:
01from pathlib import Path02from typing import Any03import tomllib040506def load_config(path: Path) -> dict[str, Any]:07try:08with path.open("rb") as file:09return tomllib.load(file)10except FileNotFoundError as error:11raise RuntimeError(f"配置文件不存在:{path}") from error12except tomllib.TOMLDecodeError as error:13raise RuntimeError(f"TOML 语法错误:{error}") from error
标准库 tomllib 只负责读取,不提供写入功能。需要生成 TOML 时可以使用 tomli-w;需要在修改文件时保留注释和排版,可以使用 tomlkit。FastAPI 服务通常在启动时读取配置,而不是在运行过程中反复改写配置文件,所以只读能力已经覆盖大多数场景。
3. 基础值
TOML 最基本的写法是 键 = 值。等号左侧是配置名称,右侧是配置内容:
1title = "FastAPI LangChain"2port = 80003temperature = 0.74debug = false
键名区分大小写。model 和 MODEL 是两个不同的键,不建议在同一个项目中混用大小写风格。
不加引号的键只能使用英文字母、数字、下划线和连字符。键名包含空格、点号等特殊字符时,需要加引号:
1model_name = "deepseek-v4-flash"2"request timeout" = 303"api.example.com" = "enabled"
引号只是键名语法的一部分。"api.example.com" 是一个完整键;不加引号的 api.example.com 则是点号键,会创建嵌套结构。
常用值可以对应到下面这些 Python 类型:
| TOML | 示例 | Python |
|---|---|---|
| 字符串 | model = "deepseek-v4-flash" | str |
| 整数 | port = 8000 | int |
| 浮点数 | temperature = 0.7 | float |
| 布尔值 | debug = true | bool |
| 带时区日期时间 | released_at = 2026-08-27T10:30:00Z | datetime |
| 本地日期 | release_date = 2026-08-27 | date |
| 本地时间 | start_at = 10:30:00 | time |
| 数组 | origins = ["http://localhost:3019"] | list |
| 表 | [server] | dict |
字符串
双引号字符串支持 \n、\t 等转义字符;单引号字符串更接近「原样保存」,适合正则表达式或 Windows 路径:
1message = "第一行\n第二行"2pattern = '^/api/v1/'3windows_path = 'C:\projects\fastapi-langchain'
多行文本可以使用三个双引号或三个单引号。配置中只有确实存在较长文本时才需要这样做,普通模型名称、URL 和文件路径使用单行字符串更容易阅读。
数字与布尔值
整数和浮点数不加引号。加上引号后,它们就会变成字符串:
1port = 80002port_text = "8000"3temperature = 0.74enabled = true
TOML 的布尔值只能写成小写的 true 或 false。True、FALSE、yes 都不是合法布尔值。
TOML 没有 null。一个字段如果没有值,通常有三种处理方式:不写该字段、提供明确默认值,或者使用业务约定的空字符串。不要写成 api_key = null,解析器会直接报错。
4. 数组与表
数组用方括号表示,元素之间使用逗号分隔:
1allow_methods = ["GET", "POST", "OPTIONS"]2retry_delays = [0.5, 1.0, 2.0]
数组可以分成多行,并允许最后一个元素后保留逗号。这种写法更适合依赖列表和较长的域名列表:
1allow_origins = [2"http://localhost:3019",3"https://langchainpy.usehook.cn",4]
TOML 1.0 允许数组包含不同类型,但应用配置中通常不建议这样做。保持同一个数组元素类型一致,更容易交给 Pydantic 校验,也更方便读者理解字段含义。
表使用 [名称] 声明。它相当于 Python 字典中的一层键:
1[server]2host = "127.0.0.1"3port = 800045[server.cors]6allow_credentials = false7allow_origins = ["http://localhost:3019"]
解析后,cors 会成为 server 下面的子字典:
01{02"server": {03"host": "127.0.0.1",04"port": 8000,05"cors": {06"allow_credentials": False,07"allow_origins": ["http://localhost:3019"],08},09},10}
点号键也可以创建嵌套结构:
1server.host = "127.0.0.1"2server.port = 8000
它与 [server] 表达的结果相同,但一个文件中最好采用一致的组织方式。配置较多时,使用表通常更清晰。
内联表
少量、紧密相关的数据可以写成内联表:
1database = { host = "localhost", port = 5432, name = "agent" }
内联表必须在一行内写完,也不能在末尾保留多余逗号。如果内容开始变长,应改成普通表。
表数组
双层方括号 [[...]] 表示「由多个表组成的数组」。例如配置多个模型:
1[[models]]2name = "deepseek-v4-flash"3provider = "openai"45[[models]]6name = "deepseek-v4-pro"7provider = "openai"
解析结果是一个字典,其中 models 对应一个列表,列表中的每一项都是字典。这种结构适合真正需要配置多份同类对象的场景。如果应用永远只使用一个默认模型,普通的 [model] 表更简单。
5. 选择
TOML、JSON、YAML 和 .env 都能保存配置,但它们解决的问题并不完全相同:
| 格式 | 更适合 | 局限 |
|---|---|---|
| TOML | Python 项目配置、工具配置、层次清晰的应用默认值 | 不支持 null,复杂数据能力有限 |
| JSON | API 数据交换、程序生成的数据 | 不支持注释,手写长配置较累 |
| YAML | Kubernetes、CI 等复杂声明文件 | 缩进和隐式类型更容易造成误解 |
.env | 密钥、部署环境变量、少量覆盖值 | 只有字符串,不适合复杂嵌套结构 |
在我们的 FastAPI + LangChain 项目中,会采用这样的分工:
pyproject.toml保存项目元数据、依赖和工具配置;config.toml保存可以提交到仓库的应用默认配置;.env保存本机或部署环境提供的密钥与覆盖值;uv.lock保存 uv 解析出的精确依赖版本,由 uv 自动维护。
配置格式不是越统一越好。把 API Key 写入 pyproject.toml,或把几十项嵌套配置压成 .env 字符串,都会让职责变得更混乱。
6. 错误
初学 TOML 时,最常见的错误不是字段含义,而是把其他格式的习惯带进来。
重复定义
同一个键或表不能定义两次:
1# 错误示例2[server]3port = 800045[server]6host = "127.0.0.1"
应把字段写进同一个 [server] 中。
缺少引号
普通字符串必须加引号:
1# 错误示例2model = deepseek-v4-flash
deepseek-v4-flash 不是数字、布尔值或日期,解析器无法判断它是什么。
错误注释
TOML 只使用 # 注释,不使用 JavaScript 的 //:
1# 这是合法注释2port = 8000 # 行尾也可以注释
路径与当前目录
Path("config.toml") 是相对于进程当前工作目录,而不是相对于 config.py 文件。开发环境从项目根目录启动时可能正常,测试或部署切换工作目录后就可能找不到文件。后续文章会使用明确的项目根路径解决这个问题。
配置不是自动生效的
即使 TOML 能成功解析,拼错字段名也不一定立即报错。直接读取字典时,错误可能一直拖到业务代码访问该字段才出现。生产项目应使用 Pydantic 模型校验配置的字段、类型和范围。