跳转到内容
新建笔记

PyYAML:配置读写、类型解析与输入验证

PyYAML 是解析和输出 YAML 的第三方 Python 包,安装名为 PyYAML,导入名为 yaml。读取普通配置优先使用 safe_load,输出普通 Python 数据使用 safe_dump;解析成功后还要验证配置是否符合程序需要的结构。本文示例以 Python 3.11、PyYAML 6.0.3 为基线。

从配置文件读入,再把字典写回文件

跳转到“从配置文件读入,再把字典写回文件”

在对应的虚拟环境中执行 python -m pip install PyYAML==6.0.3 后,可以运行下面的完整例子。它在临时目录中创建 config.yaml 和 output_data.yaml,保留原记录里的姓名、年龄、技能列表与就业状态;退出上下文后清理演示文件。

from pathlib import Path
from tempfile import TemporaryDirectory
import yaml
data = {
"name": "Jane Doe",
"age": 25,
"skills": ["Python", "Web Development", "UI/UX Design"],
"is_employed": False,
}
with TemporaryDirectory() as directory:
root = Path(directory)
config = root / "config.yaml"
config.write_text("name: Jane Doe\nage: 25\n", encoding="utf-8")
with config.open("r", encoding="utf-8") as stream:
loaded = yaml.safe_load(stream)
assert loaded == {"name": "Jane Doe", "age": 25}
print(loaded)
output = root / "output_data.yaml"
with output.open("w", encoding="utf-8") as stream:
yaml.safe_dump(data, stream, allow_unicode=True,
default_flow_style=False, sort_keys=False)
with output.open("r", encoding="utf-8") as stream:
restored = yaml.safe_load(stream)
assert restored == data
assert list(restored) == list(data)
print(output.read_text(encoding="utf-8"))
assert "中文" in yaml.safe_dump({"name": "中文"}, allow_unicode=True)

allow_unicode=True 允许输出可直接阅读的 Unicode 字符;文件仍通过 encoding="utf-8" 决定实际字节编码。default_flow_style=False 倾向使用分行缩进形式,sort_keys=False 按当前字典遍历顺序输出。safe_dump 不保证保留输入文件的注释、引号风格和排版;读入再输出不是原文格式的无损往返。

YAML 结构与 Python 对象的对应关系

跳转到“YAML 结构与 Python 对象的对应关系”

YAML 的核心结构是标量、序列和映射。时间戳、二进制及标签等是类型解析或图结构方面的规则,不是另一个与三种核心结构平行的容器分类。

原表主题PyYAML safe_load 中的结果需要区分的边界
标量 Scalarstr、bool、int、float、None 等引号、标签及隐式解析规则影响类型
序列 Sequencelist保留元素顺序,元素可以是嵌套结构
映射 MappingdictPython 3.7 起字典保证插入顺序;YAML 映射本身不应当作业务排序规则
复合类型嵌套的列表、字典和标量解析出来不等于符合应用的数据模型
时间戳 Timestamp日期可为 datetime.date,含时间可为 datetime.datetime时区及格式影响具体对象;需要纯文本日期时可加引号
二进制 Binary!!binary 通常解析为 bytesYAML 文本内使用 Base64 表示,不是直接得到普通 str
合并键 Merge KeyPyYAML 解析 << 后合并进字典属于该解析器支持的合并语义,不是可随意跨版本假定的 YAML 核心规则
锚点与别名 Anchor/Alias多个位置可以引用同一个 Python 对象不是自动深复制,修改可变对象会影响其他引用
标签 Tag决定如何构造节点的对象safe_load 支持标准安全类型,未知或 Python 对象标签会报错

下面把容易误判的类型放在一个可检查的文档里:

from datetime import date, datetime, timezone
import yaml
text = """
name: Jane Doe
enabled: false
count: 25
ratio: 1.5
missing: null
skills: [Python, Web Development]
profile: {city: Shanghai}
day: 2026-10-03
moment: 2026-10-03T08:30:00Z
payload: !!binary aGVsbG8=
literal_yes: !!str yes
quoted_day: "2026-10-03"
implicit_yes: yes
"""
value = yaml.safe_load(text)
assert value["name"] == "Jane Doe"
assert value["enabled"] is False and type(value["count"]) is int
assert value["ratio"] == 1.5 and value["missing"] is None
assert value["skills"] == ["Python", "Web Development"]
assert value["profile"] == {"city": "Shanghai"}
assert type(value["day"]) is date
assert value["moment"] == datetime(2026, 10, 3, 8, 30, tzinfo=timezone.utc)
assert value["payload"] == b"hello"
assert value["literal_yes"] == "yes" and value["quoted_day"] == "2026-10-03"
assert value["implicit_yes"] is True
print("YAML type checks passed")

PyYAML 6.0.3 默认使用的一些隐式解析规则来自 YAML 1.1,例如未加引号的 yes、on 可能变为布尔值。不要直接套用其他 YAML 1.2 解析器的结果;配置里确实需要这些字符串时,明确加引号或 !!str。对应行为见 PyYAML 文档与 PyYAML 项目版本信息。

锚点、别名和合并不会自动隔离嵌套对象

跳转到“锚点、别名和合并不会自动隔离嵌套对象”

合并键让配置复用默认值;别名让节点共享引用。下面的 override 在普通 safe_load 下覆盖默认超时,但嵌套列表仍可能共享。

import yaml
config = yaml.safe_load("""
defaults: &defaults
timeout: 5
labels: [base]
alias: *defaults
override:
<<: *defaults
timeout: 10
""")
assert config["alias"] is config["defaults"]
assert config["override"] is not config["defaults"]
assert config["override"]["timeout"] == 10
assert config["defaults"]["timeout"] == 5
assert config["override"]["labels"] is config["defaults"]["labels"]
config["alias"]["labels"].append("extra")
assert config["override"]["labels"] == ["base", "extra"]
print("alias and merge checks passed")

需要独立修改时,要按数据结构选择复制策略,参见对象复制。循环别名还可能构造循环对象,因此下游遍历不能假定解析结果总是一棵没有回路的树。

安全构造、字段验证和重复键是不同检查

跳转到“安全构造、字段验证和重复键是不同检查”

safe_load 限制对象构造方式,但不会替你检查业务字段、文件大小或嵌套深度。空文档通常返回 None;顶层也可能是列表或字符串,不能读完就直接当字典使用。

import yaml
def require_profile(value):
if not isinstance(value, dict):
raise ValueError("profile must be a mapping")
if not isinstance(value.get("name"), str) or not value["name"]:
raise ValueError("name must be nonempty text")
# bool 是 int 的子类,这里明确只接受整数年龄。
if type(value.get("age")) is not int or not 0 <= value["age"] <= 150:
raise ValueError("age must be an integer between 0 and 150")
return value
assert yaml.safe_load("") is None
assert require_profile(yaml.safe_load("name: Jane Doe\nage: 25"))["age"] == 25
for text in ["", "[Jane, 25]", "name: Jane\nage: true", "name: Jane\nage: -1"]:
try:
require_profile(yaml.safe_load(text))
except ValueError:
pass
else:
raise AssertionError("invalid profile must be rejected")
for text in ["items: [1, 2", "item: !unknown value",
"!!python/object/apply:builtins.str []"]:
try:
yaml.safe_load(text)
except yaml.YAMLError:
pass
else:
raise AssertionError("invalid or unsupported YAML must be rejected")
print("construction and schema checks passed")

默认 safe_load 遇到重复映射键时可能保留后一个值。若配置不允许这种覆盖,可显式提供更严格的加载器。以下策略在合并键展开后检查重复项,因此也会拒绝通过合并后重写同名键的配置;它比上一节的默认合并规则更严格。

import yaml
from yaml.constructor import ConstructorError
class UniqueKeySafeLoader(yaml.SafeLoader):
def construct_mapping(self, node, deep=False):
if not isinstance(node, yaml.MappingNode):
raise ConstructorError(None, None, "expected a mapping", node.start_mark)
self.flatten_mapping(node)
seen = set()
for key_node, _ in node.value:
key = self.construct_object(key_node, deep=deep)
try:
duplicate = key in seen
seen.add(key)
except TypeError as error:
raise ConstructorError(None, None, "unhashable mapping key",
key_node.start_mark) from error
if duplicate:
raise ConstructorError(None, None, "duplicate mapping key",
key_node.start_mark)
return super().construct_mapping(node, deep=deep)
def load_unique(text):
return yaml.load(text, Loader=UniqueKeySafeLoader)
assert yaml.safe_load("port: 8000\nport: 9000") == {"port": 9000}
assert load_unique("port: 8000\nhost: localhost") == {"port": 8000, "host": "localhost"}
for text in ["port: 8000\nport: 9000", "outer: {a: 1, a: 2}",
"base: &base {port: 8000}\nchild: {<<: *base, port: 9000}"]:
try:
load_unique(text)
except ConstructorError:
pass
else:
raise AssertionError("duplicate keys must be rejected")
print("duplicate-key policy checks passed")

这里的 yaml.load 显式使用继承自 SafeLoader 的受限加载器;不能把它改成任意对象加载器。若键经过类型转换后相等,例如 true 与 1,此策略也视为冲突,避免在 Python 字典中静默覆盖。

一个文件里有多个文档

跳转到“一个文件里有多个文档”

safe_load 只接受一个文档;safe_load_all 逐个产生多个文档。读取文件时,要在文件仍打开的上下文中完成迭代。输出多个文档可用 safe_dump_all。

from pathlib import Path
from tempfile import TemporaryDirectory
import yaml
with TemporaryDirectory() as directory:
filename = Path(directory) / "documents.yaml"
filename.write_text("name: first\n---\nname: second\n", encoding="utf-8")
with filename.open("r", encoding="utf-8") as stream:
documents = list(yaml.safe_load_all(stream))
assert documents == [{"name": "first"}, {"name": "second"}]
encoded = yaml.safe_dump_all(documents, allow_unicode=True, sort_keys=False)
assert list(yaml.safe_load_all(encoded)) == documents
try:
yaml.safe_load(encoded)
except yaml.YAMLError:
pass
else:
raise AssertionError("single-document loader must reject multiple documents")
print("multi-document checks passed")

项目需要限制大小、必填项、字段类型或允许的标签时,应把这些要求写成单独的输入规则。数据交换如果只需要 JSON 的类型集合,也可直接使用标准库 JSON。