首页
/ CPython tomllib 模块深度解析:TOML 文件解析的标准库 API、类型映射与源码实现

CPython tomllib 模块深度解析:TOML 文件解析的标准库 API、类型映射与源码实现

2026-09-07 15:36:59作者:柏廷章Berta

tomllib 是 CPython 标准库中用于解析 TOML(Tom's Obvious Minimal Language)文件的官方模块,自 Python 3.11 引入,并在 3.15 中将支持升级到 TOML 1.1.0。本文基于仓库中的 模块文档实现源码,完整讲解 load / loads 的 API 语义、parse_float 自定义浮点解析、TOMLDecodeError 错误定位机制、TOML 到 Python 的完整类型转换表,并深入源码剖析解析主循环、键值写入规则与安全防护措施,最后结合 3.15 What's New 说明 TOML 1.1.0 新增语法的实际变化。

一、模块定位:只读解析器与版本演进

模块文档开篇即明确了 tomllib 的边界:

  • 功能:提供解析 TOML 1.1.0 文档的接口;
  • 限制:该模块不支持写入 TOML(does not support writing TOML);
  • 版本:模块随 Python 3.11 加入,初始支持 TOML 1.0.0;Python 3.15 起支持 TOML 1.1.0(见下文 第五节)。

对于需要写出 TOML 的场景,官方文档推荐搭配两个第三方包使用:

第三方包 定位 适用场景
Tomli-W(PyPI: tomli-w) 纯写端,API 风格与标准库 marshalpickle 一致 只需生成 TOML,读端可用 tomllib
TOML Kit(PyPI: tomlkit) 保留格式的读写库 编辑已有 TOML 文件、保持注释与排版

安全提示(文档原文以 warning 框强调):解析来自不可信来源的数据时要格外谨慎——恶意的 TOML 字符串可能让解码器消耗大量 CPU 和内存资源,建议限制待解析数据的大小。这一点在源码中也有对应的防御设计,见 第四节MAX_KEY_PARTS

二、核心 API 详解

2.1 tomllib.load(fp, /, *, parse_float=float)

读取并解析一个 TOML 文件,返回 dict。要点如下:

  • 第一个参数 fp 必须是可读取的二进制文件对象(binary file object),即用 open("foo.toml", "rb") 方式打开;
  • TOML 各类型按 第三节转换表 转为 Python 对象;
  • parse_float 参数(仅关键字):每个 TOML 浮点数的字符串形式都会传给该可调用对象,默认等价于 float(num_str),可换成 decimal.Decimal 等;该可调用对象不得返回 dictlist,否则抛出 ValueError
  • 遇到非法 TOML 文档时抛出 TOMLDecodeError

源码实现 可以看到其具体行为:

def load(fp: IO[bytes], /, *, parse_float: ParseFloat = float) -> dict[str, Any]:
    """Parse TOML from a binary file object."""
    b = fp.read()
    try:
        s = b.decode()
    except AttributeError:
        raise TypeError(
            "File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`"
        ) from None
    return loads(s, parse_float=parse_float)

两个细节值得注意:

  1. b.decode() 使用默认的 UTF-8 解码(TOML 规范要求 UTF-8 编码);
  2. 如果你传入了文本模式文件对象,fp.read() 返回 strstr.decode() 不存在,于是被转换为一条信息明确的 TypeError,直接提示「应以二进制模式打开文件」——这比静默出错更利于排查。

2.2 tomllib.loads(s, /, *, parse_float=float)

str 对象解析 TOML,返回 dictparse_float 语义与 load 相同。实现 中有两处规范层面的预处理:

# 规范允许(甚至在字符串字面量内)把 "\r\n" 转换为 "\n",这里统一做掉以简化解析
src = s.replace("\r\n", "\n")
  • 先把 CRLF 换行归一化为 LF,符合 TOML 规范的换行处理要求;
  • 若传入的不是 str(例如 bytes),会抛出 TypeError: Expected str object, not 'bytes' 这类带类型名的错误——因此「读文件用 load、读字符串用 loads」是硬性约定,二者不互相兜底。

parse_float 在进入解析前先经过 make_safe_parse_float 包装(见 2.4 节)。

2.3 文档示例(可直接复制运行)

以下两段示例完整继承自 模块文档

解析 TOML 文件(注意 "rb" 二进制模式):

import tomllib

with open("pyproject.toml", "rb") as f:
    data = tomllib.load(f)

解析 TOML 字符串

import tomllib

toml_str = """
python-version = "3.11.0"
python-implementation = "CPython"
"""

data = tomllib.loads(toml_str)

再补一个使用 parse_float 的高精度场景(decimal.Decimal 避免二进制浮点误差):

import tomllib
from decimal import Decimal

toml_str = 'price = 19.99\nqty = 3.5'
data = tomllib.loads(toml_str, parse_float=Decimal)
# data == {'price': Decimal('19.99'), 'qty': Decimal('3.5')}

2.4 parse_float 的安全包装

文档要求 parse_float 不得返回 dictlist源码 用装饰器方式强制了这一点:

def make_safe_parse_float(parse_float: ParseFloat) -> ParseFloat:
    # 默认的 float 永远不返回非法类型,直接透传以省开销
    if parse_float is float:
        return float

    def safe_parse_float(float_str: str) -> Any:
        float_value = parse_float(float_str)
        if isinstance(float_value, (dict, list)):
            raise ValueError("parse_float must not return dicts or lists")
        return float_value

    return safe_parse_float

禁止返回容器类型的动机写在注释里:dict / list 会与解析出的 TOML table / array 混淆,干扰解析器内部对「嵌套是否可继续写入」的判断。另外从 值解析函数 可以看到,inf-inf+nan 等特殊浮点字面量同样走 parse_float 通道——如果你传入了自定义解析器,这些字面量也会被它处理。

三、TOML → Python 类型转换表

这是原文档的核心参考表(锚点 toml-to-py-table),load / loads 的返回值严格遵循此表:

TOML Python
TOML document dict
string str
integer int
float float(可用 parse_float 自定义)
boolean bool
offset date-time datetime.datetimetzinfodatetime.timezone 实例)
local date-time datetime.datetimetzinfoNone
local date datetime.date
local time datetime.time
array list
table dict
inline table dict
array of tables list of dict

结合 日期时间源码 可以进一步确认表中语义的实现细节:

  • offset date-time:带 Z/z 时区用 timezone.utc;带 ±HH:MM 偏移时通过 cached_tz 构造 timezone 实例,且该函数带 @lru_cache(maxsize=None)——源码注释解释了为何无需限制缓存大小:能匹配上正则的偏移组合最多只有 24 × 60 × 2 = 2880 种,缓存天然有界;
  • local date-time:正则匹配到时间但没有时区信息时,tzinfo 置为 None
  • local date:只有日期部分(无时间部分)时返回 datetime.date 而非 datetime
  • 整数:数字正则(见下)匹配后若不含 floatpart 分组,则调用 int(match.group(), 0) 转换——base=0 表示自动识别 0x 十六进制、0o 八进制、0b 二进制前缀,与 Python 字面量规则一致。

数字与时间的识别依赖 Lib/tomllib/_re.py 中编译好的正则:

RE_NUMBER: Final = re.compile(
    r"""
0
|(?:
    x0-9A-Fa-f*   # hex
    |
    b01*                 # bin
    |
    o0-7*               # oct
)
|
[+-]?(?:0|1-9*)         # dec, integer part
(?P<floatpart>
    (?:\.0-9*)?         # optional fractional part
    (?:[eE][+-]?0-9*)?  # optional exponent part
)
""",
    flags=re.VERBOSE,
)

即:整数部分禁止前导零、下划线分隔符只允许出现在数字之间;只要出现小数部分或指数部分,整串就会交给 parse_float 处理。

四、解析主循环与写入规则源码剖析

loads 的核心是一个「逐条语句」的主循环,源码结构 非常清晰,每个循环迭代处理一行语句:

  1. 跳过行首空白skip_chars(src, pos, TOML_WS));
  2. 分派语句类型:文件结尾 / 空行 / 注释 / key = value 键值对 / [table] 建表 / [[array of tables]] 追加数组表;
  3. 跳过行尾注释skip_comment);
  4. 期望行尾或文件结尾,否则抛出 TOMLDecodeError("Expected newline or end of document after a statement", ...)

其中 [ 开头的语句通过再看第二个字符区分单表与数组表(源码):[[create_list_rule,单个 [create_dict_rule

4.1 Flags:防止重复声明与覆盖的位标记系统

TOML 有几条关键不变量,实现上由 Flags 类 用两个标志位维护:

  • FROZEN:标记不可变命名空间(inline array / inline table);
  • EXPLICIT_NEST:标记已显式创建的嵌套表,之后不能再被 [table] 语法打开。

典型检查逻辑见 create_dict_rule:同一张表声明两次(Cannot declare [a, b] twice)、往已有非表值上继续建表(Cannot overwrite a value)都会在这里被拦截;而 key_value_rule 则保证点键(dotted key)不会重定义已有表、不会写入 FROZEN 命名空间,且解析出的 dict/list 值会递归地把整棵子树标记为 FROZEN。

4.2 NestedDict:结果树的构建

结果结构由 NestedDict 维护:

  • get_or_create_nest(key):沿键路径下钻,缺失的中间层自动创建为 {};若路径中途遇到 list(数组表的值),则自动下钻到最后一个元素 cont[-1]——这正是「[a.b] 出现在 [[a]] 之后时,后续键值写入最新一个数组表元素」这一 TOML 行为的实现;
  • append_nest_to_list(key):处理 [[a.b]],键已存在则向列表追加新 {},不存在则初始化为 [{}]

4.3 值解析的分派顺序

parse_value 按「检查速度与出现概率」排序做分派,值得注意的顺序是:

  1. " / ' 开头 → 单行或多行 basic / literal 字符串;
  2. true / false 前缀匹配布尔;
  3. [ → 数组,{ → 内联表;
  4. 先匹配日期时间正则,再匹配本地时间,最后才匹配数字——源码注释明确说明:数字正则是贪婪的,任何以十进制数字开头的类型它都能吃掉,所以必须放在日期/时间之后;
  5. 特殊浮点 ±inf / ±nanparse_float
  6. 都不匹配则 TOMLDecodeError("Invalid value", ...)

字符串方面,basic 字符串支持 \b \t \n \f \r \e \" \\ 以及 \xHH / \uHHHH / \UHHHHHHHH 十六进制转义(is_unicode_scalar_value 会校验其确为合法 Unicode 标量值);多行字符串支持「反斜杠+换行」的空白折叠转义。

4.4 针对恶意输入的两道防线

与文档中的安全警告相呼应,源码里有两处显式防护:

  • 键段数上限源码):
# 路径性过长的键段数会引发二次方复杂度(例如 Flags.is_)。
# 虽然当前解析键并不用递归,但键命名了一个递归结构,
# 因此用 getrecursionlimit() 与 RecursionError 来限制它是合理的。
MAX_KEY_PARTS: Final = sys.getrecursionlimit()

点键段数超过 sys.getrecursionlimit() 时抛出 RecursionError,阻断二次方复杂度退化;

  • parse_float 包装(见 2.4 节):防止自定义解析器污染解析器的容器判断。

五、TOML 1.1.0 新特性(Python 3.15 起)

What's New in Python 3.15tomllib 小节(锚点 whatsnew315-tomllib-1-1-0)说明了这次升级是向后兼容的:所有合法的 TOML 1.0.0 文档解析结果不变。按官方 TOML 变更记录,具体新增语法有三类:

  1. 内联表允许换行与尾随逗号。此前内联表必须写在单行且不能以逗号结尾,现在以下写法合法:
tbl = {
   key      = "a string",
   moar-tbl =  {
      key = 1,
   },
}
  1. basic 字符串新增 \xHH\e 转义\xHH 针对码点小于 255 的字符):
null = "null byte: \x00; letter a: \x61"
csi = "\e["
  1. 日期与时间中的秒变为可选
dt = 2010-02-03 14:15
t  = 14:15

这三项能力分别对应源码中的 parse_inline_table(通过 skip_comments_and_array_ws 跳过换行与逗号,天然容忍尾随逗号)、parse_basic_str_escape\\x 分支调用 parse_hex_char,长度 2)以及 RE_DATETIME / _TIME_RE_STR(秒与小数秒均为可选分组)。

六、TOMLDecodeError:错误信息的定位机制

文档定义的异常签名与属性:

TOMLDecodeError(msg, doc, pos)

它是 ValueError 的子类,附带以下属性:

属性 含义
msg 未格式化的错误消息
doc 正在解析的 TOML 文档
pos 解析失败处在 doc 中的索引
lineno pos 对应的行号
colno pos 对应的列号

版本说明:

  • 3.14:新增 msgdocpos 三个参数及上述五个属性;
  • 3.14 起弃用:以自由位置参数方式传参已弃用(会触发 DeprecationWarning,见 源码 中的 DEPRECATED_DEFAULT 哨兵逻辑)。

实现 可见行列号是如何从 pos 推导的:

lineno = doc.count("\n", 0, pos) + 1
if lineno == 1:
    colno = pos + 1
else:
    colno = pos - doc.rindex("\n", 0, pos)

if pos >= len(doc):
    coord_repr = "end of document"
else:
    coord_repr = f"line {lineno}, column {colno}"
errmsg = f"{msg} (at {coord_repr})"

str(exc) 会得到形如 Expected '=' after a key in a key/value pair (at line 2, column 5) 的消息,失败位置在文档末尾时显示为 (at end of document)。这意味着捕获异常后可以直接用 exc.lineno / exc.colno 做 IDE 式报错或日志定位,而 exc.doc 让你无需重新持有原文即可输出完整上下文。

模块包结构一览(纯 Python 实现,无 C 扩展):

文件 职责
Lib/tomllib/init.py 公开 loadsloadTOMLDecodeError,并把异常的 __module__ 伪装为本包,使 TOMLDecodeError 看起来定义于 tomllib
Lib/tomllib/_parser.py 解析主循环、键/表/值规则、FlagsTOMLDecodeError;通过 __lazy_modules__ 延迟导入 _re
Lib/tomllib/_re.py 数字/日期/时间正则及匹配结果到 int / float / datetime 的转换
Lib/tomllib/_types.py ParseFloatKeyPos 等类型别名

行为与错误路径的回归覆盖在 Lib/test/test_tomllib/ 测试包中(如 test_data.pytest_error.pytest_misc.py),可作实现细节的进一步验证入口。

七、实践要点小结

  1. 二进制模式打开文件open(path, "rb")tomllib.load;字符串走 tomllib.loads,传错类型会收到明确的 TypeError 提示;
  2. 高精度金额/坐标parse_float=Decimal,且记住 ±inf±nan 也走该回调;
  3. 错误处理:捕获 TOMLDecodeError(而非笼统的 ValueError),直接读 lineno / colno 属性定位问题行;以 3.14+ 的 msg / doc / pos 三参数构造异常,避免使用已弃用的自由位置参数;
  4. 不可信来源:限制输入体积后再解析,源码中的 MAX_KEY_PARTS 只能防键深度攻击,不能替你挡下所有恶意负载;
  5. 需要写出或保格式编辑 TOMLtomllib 只管读,写端按文档建议选用 Tomli-WTOML Kit
  6. 版本前提:本文涉及 TOML 1.1.0 语法示例需 Python 3.15+;TOMLDecodeError 的属性化签名需 3.14+;模块本身需 3.11+。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391