第18篇|类型注解与 dataclass、Pydantic
本篇定位:《Python 教程》专栏二「Python 进阶与工程」第 18 篇。承接第 07~08 篇的函数签名、第 12~14 篇的类与数据模型、第 16~17 篇的装饰器与上下文边界,把 类型注解(type hints)、
@dataclass与 Pydantic 讲清:静态合同 vs 运行时校验、typing常用积木、数据类减样板、边界处强制形状。为第 19 篇标准库、第 22 篇测试、第 26~28 篇 HTTP / FastAPI / Agent 打底。读完你将能够:给函数与类字段写出可读注解;区分「IDE / mypy 检查」与「Pydantic 拒收非法输入」;用
list[str]、X | None、Callable、Protocol、TypedDict;用@dataclass生成init/repr并避开可变默认陷阱;用 PydanticBaseModel做解析与字段约束;说明三者在 FastAPI / Agent 工具参数中的分工。预计用时:阅读 35~45 分钟,动手 25~35 分钟。
第 16、17 篇把「函数级横切」和「块级资源」立成协议;本篇转向另一把工程刀具——给数据与函数签名加上可检查的合同。动态类型让 Python 写起来快,但一旦函数跨模块、跨团队、跨 HTTP / LLM 边界,dict 里到底有哪些键、温度能不能是负数,全靠约定就会碎。
类型注解回答「期望什么形状」;静态检查器(mypy、pyright、IDE)在运行前读合同;dataclass 减少手写样板;Pydantic 在门口把非法输入挡下。三者常一起出现:内部配置用 dataclass,对外 API / 工具入参用 Pydantic,函数签名处处写注解方便补全。
一句话定心:注解是合同文档;静态检查读合同;Pydantic 在边界验合同。 不要指望「写了 : int 运行时就会拒绝字符串」——那是校验库的工作,不是注解本身的魔法。
建议边读边开练习目录(uv init --no-package,需要时 uv add pydantic)。阅读顺序:心智模型 → typing 积木 → 函数与结构化类型 → dataclass → Pydantic → AI Infra 场景 → 踩坑。下一篇预告标准库精选(19):collections / itertools / functools 会大量出现带注解的工厂与偏函数。
和第 07 篇对照:参数列表多了「形状」;和第 12 篇对照:类属性从「随便挂」变成「声明字段」;和第 14 篇对照:数据模型不止魔术方法,还有类型层合同;和第 16~17 篇对照:装饰器与上下文管行为边界,本篇管数据形状。
动手时建议沿用「聊天配置 / 工具参数」意象:先给函数加注解,再写成 dataclass,再改成 Pydantic 并故意传坏数据看报错。做完后打开 FastAPI 或任一 Agent 框架的 tool schema 文档,只是换皮。
本篇不穷尽 TypeVar 高阶技巧、不讲完整的 mypy 配置矩阵,也不展开 Pydantic v1 / v2 每一处迁移细节(示例以 Pydantic v2 记法为准:model_validate、Field)。主线是:合同心智 → 常用写法 → 数据类 → 运行时校验 → 工程选型。和「到处 dict 裸奔」相比,类型化的价值是:可读、可补全、可在边界拒收。
写注解时保持一个工程直觉:对内从宽、对外从严——模块内部可用较松的注解;跨进程、跨 HTTP、跨 LLM 的入口必须校验。社区里那些「全项目 Any」的仓库短期痛快,长期排障成本会回来找你。
也可以把类型看成空间维度上的协议:第 15 篇回答「值如何随时间流式产生」;第 16 篇回答「调用如何被增强」;第 17 篇回答「块如何获取与释放」;本篇回答「值长什么样」。形状约束从「注释里写一句」搬到「机器可读」——这正是框架喜欢注解的原因。
读官方 typing 文档与 Pydantic 快速入门时,按本篇目录对照即可。入门阶段把「能写、能读错误、能选型」做扎实,比背完整类型代数更重要。
若你刚从第 17 篇「把 try/finally 收成 with」的热身过来,本篇前两节会换一种爽感:同样是工程化,但挂钩点从「资源生命周期」换成「值的形状」。把聊天配置从裸 dict 改成带注解的函数参数,再升到 dataclass,最后在门口换成 Pydantic——三条路径走完,选型直觉就长出来了。
写注解时再记一条:它首先是给人与工具读的文档,其次才是检查器的输入。注释里写「messages 是列表」会过期;写在签名上的 list[Msg] 会随重构一起被 IDE 提醒。框架作者正是靠这一点,才敢在文档里只贴一个模型类名。
和「把类型写在 Wiki 里提醒同事」相比,注解的价值是:工具链帮你执行提醒——漏改调用点会在检查器里冒红;对 Pydantic 而言,漏字段在请求进入业务前就失败。排障时你面对的是结构化错误,而不是神秘的 KeyError。
一、类型注解心智模型
1.1 注解存哪、何时被看

图1 类型注解心智模型:合同,不是运行时魔法
def greet(name: str, times: int = 1) -> str:
return (name + "!") * times
print(greet.__annotations__)
{'name': <class 'str'>, 'times': <class 'int'>, 'return': <class 'str'>}
要点:
- 注解写在参数与返回值旁,运行时默认几乎不检查——
greet(123)仍可能跑起来(或稍后炸)。 - 注解进入
annotations,供 IDE、类型检查器、运行时库(Pydantic、FastAPI)读取。 - 从 Python 3.9 / 3.10 起,优先写
list[str]、dict[str, int]、str | None,少用旧的List、Optional(能读懂旧代码即可)。
def bad(x: int) -> int:
return x + 1
# 运行时不会因为类型「不对」而拒绝:
print(bad("hi")) # 可能 TypeError,但是算术炸的,不是注解拦的
1.2 静态检查 vs 运行时校验
AI Infra 视角:训练超参可以先
dataclass;推理服务 HTTP 体、Agent function call 参数必须 Pydantic(或等价校验)。LLM 返回的 JSON「看起来像」不够,边界上要验。
1.3 和「鸭子类型」如何共存
Python 仍然是鸭子类型语言:有 read 就能当文件用。注解用 Protocol 描述「需要哪些方法」,不必强迫继承。静态检查器按结构判定;运行时仍靠鸭子。
1.4 渐进采用,而不是大爆炸重写
团队仓库若已有上万行无注解代码,正确姿势不是停工两周「全部标完」,而是:
- 新模块、新 PR 必须带注解;
- 改到的旧函数顺手补全;
- 边界(HTTP、队列、工具调用)优先上校验库;
- 用检查器的「仅改动文件」模式降低噪音。
类型债务和测试债务一样,靠持续偿还。本篇目标是让你会写、会选、会在边界设防,而不是成为类型理论专家。
二、typing 常用积木
2.1 容器、可选、联合

图2 typing 常用积木一览
def summarize(
texts: list[str],
max_len: int | None = None,
meta: dict[str, str] | None = None,
) -> str:
body = " ".join(texts)
if max_len is not None:
body = body[:max_len]
return body
list[str]:元素是str的列表(不变异约定仍靠文档;要不可变用Sequence)。int | None:可有可无;在用之前通常先分支判断。dict[str, str]:键值类型;复杂结构可升TypedDict或模型类。
2.2 Callable 与装饰器签名
from collections.abc import Callable
Handler = Callable[[str], str]
def apply(h: Handler, s: str) -> str:
return h(s)
print(apply(str.upper, "hi"))
HI
第 16 篇的包装器常写成 Callable[..., T];精确参数列表能帮 IDE 提示,但过度精确有时难维护——在公开 API 上优先写清。
2.3 Literal、TypedDict、Protocol
from typing import Literal, Protocol, TypedDict
class Msg(TypedDict):
role: Literal["system", "user", "assistant"]
content: str
class Closable(Protocol):
def close(self) -> None: ...
def send(messages: list[Msg]) -> None:
assert messages[0]["role"] in ("system", "user", "assistant")
Literal:把字符串 / 数字收成枚举式取值(配置开关很常见)。TypedDict:给「仍是 dict」的结构加键级注解(JSON 中间层好用)。Protocol:结构化子类型——有方法即兼容,对应鸭子合同。
2.4 变量注解与推迟求值
from __future__ import annotations # 可选:注解当字符串存,避免前向引用问题
class Node:
def __init__(self, child: Node | None = None) -> None:
self.child = child
count: int = 0
cache: dict[str, Node] = {}
类体内引用尚未定义完的类名时,from future import annotations 或引号 "Node" 都能缓解。Python 3.12+ 对不少场景更友好,但团队统一风格更重要。
2.5 TypeVar 浅尝(够用即可)
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
TypeVar 让「传入什么元素,返回什么元素」在检查器里保持一致。入门阶段见到即可;写通用装饰器、容器工具时再加深。切忌一上来给每个函数都发明类型变量——可读性会先垮。
三、给函数与类写好注解
3.1 返回值、生成器、异步(预告)
from collections.abc import Iterator
def countdown(n: int) -> Iterator[int]:
while n > 0:
yield n
n -= 1
生成器返回 Iterator[T] / Generator[Yield, Send, Return];async def 返回 awaitable(第 25 篇再展开)。先养成「有返回就写 ->」的习惯。
3.2 方法与 self / cls
class Counter:
def __init__(self, start: int = 0) -> None:
self.value = start
def inc(self, step: int = 1) -> int:
self.value += step
return self.value
@classmethod
def from_str(cls, s: str) -> "Counter":
return cls(int(s))
self / cls 通常不需要注解;返回实例时写类名(或 Self,3.11+)。
3.3 渐进标注策略
不要幻想一天标完全仓库。推荐:
- 新代码、公共函数先标;
- 边界(IO、API、工具)优先;
- 内部私有辅助可稍后;
- CI 里对改动文件开检查,逐步收紧。
AI Infra 视角:
TOOLS注册表若保存「名字 → 可调用」,给 handler 一个Protocol或Callable别名,比到处Any更利于第 28 篇最小 Agent 排错。
3.4 注解风格约定(团队向)
- 公开函数必有返回注解(包括
-> None)。 - 容器写清元素类型;实在异构再用
object并文档说明。 - 少用裸
Any;过渡期写明为什么。 - 与运行时校验库配合时,以模型字段为准,函数注解与之对齐,避免两套真相。
这些约定没有唯一正确答案,但仓库内一致比「个人完美品味」更重要。
四、@dataclass:声明式数据容器
4.1 从样板到声明

init、repr、eq等方法的过程。左侧为用户写的代码,@dataclass装饰的Config类,包含model、temp、tags字段。右侧是自动生成的内容,包括__init__、__repr__、__eq__方法,以及可选的frozen、order、slots等。下方还列出了三个问题:用default_factory有可变默认陷阱、frozen=True不可变配置对象、不校验取值仍能构造。该图与上下文紧密相关,直观呈现了@dataclass的工作原理及常见问题。图3 @dataclass:声明字段,生成样板方法
from dataclasses import dataclass, field
@dataclass
class ChatConfig:
model: str
temperature: float = 0.7
tags: list[str] = field(default_factory=list)
cfg = ChatConfig(model="mini")
print(cfg)
print(cfg == ChatConfig("mini", 0.7, []))
ChatConfig(model='mini', temperature=0.7, tags=[])
True
装饰器根据注解字段生成 init、repr、eq 等。字段顺序:无默认值的在前,有默认值的在后。
4.2 field、default_factory、frozen
坑的根源:可变默认值不能写 tags: list[str] = [](与普通函数默认值同一类问题)。用 field(default_factory=list)。
@dataclass(frozen=True)
class Endpoint:
host: str
port: int = 443
ep = Endpoint("api.example.com")
# ep.port = 80 # FrozenInstanceError
frozen=True 适合配置对象;需要排序可加 order=True;Python 3.10+ 可用 slots=True 减内存、防瞎加属性。
4.3 dataclass 不做什么
它不验证 temperature 是否在 0~2。构造 ChatConfig("x", -1) 仍然成功。要约束取值,请:
- 手写
post_init里raise;或 - 换用 Pydantic(推荐对外边界)。
@dataclass
class BoundedTemp:
temperature: float
def __post_init__(self) -> None:
if not 0 <= self.temperature <= 2:
raise ValueError("temperature out of range")
4.4 与普通类如何分工
普通类适合有丰富行为(方法很多、继承体系、可变状态机);dataclass 适合「主要是数据、偶尔一两个小方法」。若发现数据类里堆了二十个业务方法,说明它可能该变回普通类,或把行为拆到函数 / 服务对象里。
配置、DTO、事件记录、简单值对象——这些是 dataclass 的舒适区。领域服务、连接池、Agent 运行时——通常不是。
五、Pydantic:运行时校验与解析
5.1 最小模型

图4 Pydantic:解析 → 校验 → 模型实例
from pydantic import BaseModel, Field, ValidationError
class ChatConfigModel(BaseModel):
model: str
temperature: float = Field(default=0.7, ge=0, le=2)
max_tokens: int = Field(default=256, gt=0)
obj = ChatConfigModel.model_validate({"model": "mini", "temperature": "0.2"})
print(obj)
print(obj.temperature, type(obj.temperature))
model='mini' temperature=0.2 max_tokens=256
0.2 <class 'float'>
注意:字符串 "0.2" 被解析成 float——这是与 dataclass 的关键差异之一。
try:
ChatConfigModel.model_validate({"model": "mini", "temperature": 9})
except ValidationError as e:
print(e.error_count())
5.2 嵌套模型与 JSON
from typing import Literal
class Message(BaseModel):
role: Literal["system", "user", "assistant"]
content: str
class ChatRequest(BaseModel):
messages: list[Message]
config: ChatConfigModel = Field(default_factory=ChatConfigModel)
req = ChatRequest.model_validate(
{
"messages": [{"role": "user", "content": "hi"}],
"config": {"model": "mini"},
}
)
print(req.model_dump()) # 回到 dict,便于序列化
model_validate 吃 dict;model_validate_json 吃 JSON 字符串;model_dump / model_dump_json 导出。嵌套会递归构造子模型。
5.3 与 dataclass 怎么选
也可以 pydantic.dataclasses.dataclass 给数据类加校验,但入门先分清「标准库 dataclass」与「BaseModel」两条路即可。
5.4 安装与版本
uv add pydantic
uv run python -c "import pydantic; print(pydantic.VERSION)"
本系列示例按 v2 API。若看到 parse_obj / .dict(),那是 v1 记法,阅读旧博客时注意换算。
5.5 自定义校验与模型配置(点到为止)
from pydantic import field_validator
class StrictName(BaseModel):
name: str
@field_validator("name")
@classmethod
def strip_nonempty(cls, v: str) -> str:
v = v.strip()
if not v:
raise ValueError("name empty")
return v
字段级约束能表达的,优先 Field(ge=...);跨字段规则再用 model 级 validator。model_config 可控制额外字段是否禁止、字符串是否自动剥离等——等你在 FastAPI 篇遇到具体需求再查文档即可,不必本篇背完。
六、工程实践与 AI Infra
6.1 工具参数 → JSON Schema

图5 AI Infra:类型如何贯穿工具与服务
class SearchArgs(BaseModel):
query: str = Field(min_length=1, max_length=200)
top_k: int = Field(default=5, ge=1, le=50)
print(SearchArgs.model_json_schema()["properties"].keys())
Agent 框架常把该 schema 交给模型做 function calling;执行前再用 SearchArgs.model_validate(arguments)——同一份合同既文档又校验。
6.2 FastAPI 预告(第 27 篇)
FastAPI 路由参数若标注为 Pydantic 模型,框架自动:解析 body、校验、生成 OpenAPI、失败返回 422。你在本篇学会的 BaseModel,到第 27 篇几乎原样粘贴。
6.3 与装饰器、上下文组合
# 伪代码意象:外层会话上下文,内层带类型的工具调用
# with Session() as sess:
# args = SearchArgs.model_validate(raw)
# result = sess.call_tool("search", args)
第 16 篇 @tool 负责注册;第 17 篇 with 负责会话;本篇模型负责参数形状。第 28 篇最小 Agent 会把三件套收拢。
6.4 静态检查怎么跑(可选)
uv add --dev pyright
uv run pyright your_module.py
不必本篇就上全量 CI;先让 IDE 红线能看懂,再在公共模块开严格模式。
AI Infra 视角:vLLM / SGLang 的采样参数、批处理请求体,本质都是「带约束的配置对象」。先在自己的小模型上练
Field(ge=...),读服务源码时只是字段更多。
6.5 小结式对照:三层防护
- 签名注解:人与 IDE 的第一道文档;
- 静态检查:CI / 本地发现形状漂移;
- Pydantic(或等价):不信任的数据入口强制解析。
缺少任一层都能活,但线上事故往往出在「只写了注释、既没有检查也没有校验」。专栏二后面的 FastAPI 与 Agent 实战,会默认你已经接受这三层思维。
七、踩坑大全
通用
坑 1:以为注解会在运行时强制类型。 现象:def f(x: int) 传入字符串仍进入函数。原因:注解默认不执行检查。解决:边界用 Pydantic / 显式校验;或对内信任 + 静态检查。
坑 2:可变默认 tags: list[str] = []。 现象:多实例共享同一列表。解决:field(default_factory=list);Pydantic 里用 Field(default_factory=list)。
坑 3:混淆 list 与 list[str] 的运行时。 现象:isinstance(x, list[str]) 在部分版本行为不符合直觉。解决:运行时用 isinstance(x, list) 再抽查元素,或交给 Pydantic。
坑 4:dict 注解过于粗糙。 现象:键名拼错到线上才发现。解决:TypedDict 或模型类;对外 JSON 优先 Pydantic。
坑 5:Pydantic v1 / v2 API 混用。 现象:parse_obj、model_validate 到处复制报错。解决:锁定主版本;新代码统一 v2。
坑 6:在 dataclass 里写校验却忘了不可变场景。 现象:frozen=True 时 post_init 改字段需 object.setattr。解决:校验只 raise;或改用 Pydantic。
静态检查器
坑 7:满屏 Any 等于没标。 现象:检查器全绿但毫无价值。解决:公共 API 避免 Any;过渡期用 # type: ignore 要写原因注释。
坑 8:循环导入因注解触发。 现象:模块顶层导入类型导致环。解决:from future import annotations、typing.TYPE_CHECKING 下导入。
macOS / Linux / Windows
坑 9:编辑器与 CI 检查器不一致。 现象:本地 IDE 不报、CI mypy 报(或相反)。解决:团队固定一种(如 pyright),版本写入 dev 依赖。
坑 10:路径与配置文件编码。 现象:从 JSON 读配置时中文或 BOM 导致解析失败。解决:统一 UTF-8;用 model_validate_json 明确走 JSON 路径。
与框架叠加
坑 11:把内部 dataclass 直接当 FastAPI 请求体。 现象:缺少校验与 OpenAPI 细节。解决:对外 Pydantic,对内可再转 dataclass(或全程模型)。
坑 12:LLM 返回多造字段 / 缺字段。 现象:偶发 ValidationError。解决:模型侧收紧 schema;代码侧捕获校验错误并重试或降级;model_config 关注额外字段策略。
八、小结
合上文章后,试着用自己的话说明:为什么 x: int 拦不住错误字符串,以及何时上 Pydantic。讲得清,才算真的会了。动手至少跑通:带注解的函数、一个 dataclass 配置、一个会拒绝非法温度的 Pydantic 模型。
- 注解是合同:写入
annotations,供人与工具阅读;默认不在运行时强制。 - 常用积木:
list[str]、dict[...]、X | None、Callable、Literal、TypedDict、Protocol。 dataclass:声明字段,生成init/repr/eq;用default_factory;frozen保配置。- Pydantic:解析 + 校验 + 嵌套模型;
Field约束;ValidationError带路径。 - 选型:对内结构用 dataclass;边界数据用 Pydantic;全程写注解利补全。
- AI Infra:工具参数 schema、FastAPI 请求体、推理配置,都是同一套合同思维。
把故事连起来:第 07~08 篇函数 → 第 12~14 篇类与数据模型 → 第 16~17 篇行为边界 → 本篇把数据形状立成合同。专栏二工程刀具继续成套:流式(15)→ 横切(16)→ 资源(17)→ 类型(18)→ 标准库(19)。
若只能带走一句话:注解描述期望;Pydantic 在门口强制期望。
也可以用「三个不要」自检:不要指望注解当校验器;不要给 list 字段写 = [];不要在 HTTP / 工具边界继续裸 dict。
再留一个自测:说出 dataclass 与 Pydantic 各一项差异;把 temperature: float 加上 0~2 约束;给 search(query, top_k) 写出模型并 model_validate——三问都顺,本篇就站稳了。
本篇技能会立刻复利:读框架先看函数签名与模型类;写 Agent 时工具参数先建模型;写服务时请求体先校验。后续标准库、测试、FastAPI,都是在「有形状的数据」上继续加能力。
和专栏一串起来复习也不亏:函数参数(07)是注解挂载点;类字段(12)是 dataclass 的前身;异常(11)解释 ValidationError 怎么处理;装饰器 / 上下文(16~17)提供行为边界——本篇是专栏二把「数据合同」立成一等工具的一课。
也可以把收获压成检查清单:能写基本注解;能解释静态 vs 运行时;能用 dataclass;能用 Pydantic 拒非法输入;能举一个 AI Infra 中的模型例子。五项过关,即可进入标准库精选篇。
若把本篇压成给同事的三十秒讲解:先画「注解 → 检查器 / 校验库」;再对比「dataclass 减样板、Pydantic 守门」;最后强调「对外从严」。三张草图比贴一篇博客更管用。等对方能独立写出带 Field(ge=0) 的模型并解释为什么 x: int 拦不住字符串,就可以一起 code review 你们项目里的配置与请求体了。
动手清单再压缩一版:标一个函数;建一个 dataclass;建一个会失败的 Pydantic 用例;把失败信息读给人听。四步做完,比只「看完文章」扎实一个数量级。
九、课后练习
- (基础) 为函数
def clamp(x, lo, hi)补全参数与返回值注解,并写两个调用示例。 - (基础) 定义
dataclassUser(id: int, name: str, tags: list[str]),正确使用default_factory;打印实例与相等比较。 - (基础) 安装 Pydantic,写
ChatConfigModel,使temperature仅允许0~2;分别传入合法与非法值观察结果。 - (理解) 用
TypedDict描述消息{role, content},再写函数接受list[Msg];思考与 PydanticMessage模型的差异。 - (理解) 定义
ProtocolGreeter(有greet(self, name: str) -> str),写两个无关类都满足协议,并写一个接受Greeter的函数。 - (理解) 嵌套模型:
ChatRequest(messages, config);从 dict 构造后model_dump();故意缺字段看ValidationError结构。 - (进阶) 给第 16 篇风格的
@timer包装器补上较合理的Callable注解(可用ParamSpec/TypeVar或简化版)。 - (进阶 · AI Infra) 设计
SearchArgs(query、top_k),打印model_json_schema();模拟 LLM 回传错误 JSON,校验失败后返回可给模型看的错误摘要(字符串即可)。讨论:schema 应该「尽量严」还是「多可选字段」更利于工具调用稳定性?
下一篇预告
下一篇是《第19篇|标准库精选:collections、itertools、functools》——在类型合同立住之后,回到「电池自带」的高频工具:Counter / defaultdict / deque、迭代器代数、partial / lru_cache / singledispatch。你会看到:注解描述形状,标准库提供高效积木;二者一起,少造轮子。
专栏二后续路线:标准库精选 → 虚拟环境与打包 → 日志调试性能 → pytest → 并发与 asyncio → HTTP / FastAPI → 最小 Agent。带着本篇的合同直觉,读源码时先看签名与模型类,再跟实现。
若想提前热身:用 collections.Counter 统计本篇练习里的词频,并给函数加上 Counter[str] 或 dict[str, int] 注解;试写 functools.lru_cache 包装的纯函数,观察缓存与类型如何共存。做完后第 19 篇谈容器与高阶函数时你会带着具体对比感。
也可以把第 16 篇的 @tool、第 17 篇的会话上下文与本篇模型对照:装饰器收集元数据,上下文管理生命周期,类型描述参数——Agent 三件套的轮廓已经清晰,标准库篇会补上日常数据结构与函数工具。
专栏一用十四篇把「对象与协议」立住;专栏二从迭代器(15)、装饰器(16)、上下文(17)走到类型(18),下一站是标准库精选。下一篇见。
若时间充裕,再多做一件事:打开你写过的脚本,找出三处「裸 dict 配置」或「函数无注解」,改成 dataclass / 注解,并在一处真正的输入边界加上 Pydantic。改完后你会更笃定:专栏二不是要你背更多语法,而是把已经会的函数与类,接到真实工程的数据合同上。
进入专栏二至此,四把刀具已经齐:生成器管「流」、装饰器管「横切」、上下文管「资源」、类型管「形状」。下一篇开始把标准库里最常用的容器与函数工具收进口袋,工程味道会更顺手。若某一节读着抽象,回到对应插图把箭头走一遍,再手打示例——多数卡住的地方,其实是「静态合同」与「运行时校验」被混成一件事。
写 post_init 或 Pydantic validator 时再记一条:错误信息要指向字段与期望,不要只抛「invalid」。调用方(含 LLM 重试环)靠清晰错误才能改下一跳输入。和「日志里打一串堆栈就算了」相比,结构化校验错误是可机读的合同反馈。
也可以把本篇看成专栏二的「数据面」补课:前面三篇把控制流与生命周期的协议立住,本篇把值的形状协议立住。后面标准库篇会给你更多「带类型的积木」;测试篇会教你如何为模型与校验写断言;FastAPI 与 Agent 篇则把合同直接接到网络与工具调用上。
若阅读时觉得「静态类型」和「Pydantic」总搅在一起,回到图 1:左边是文档与检查器,右边是门口卫兵。日常编程两者都要,但职责不同——混谈是多数初学者卡住的根源。
收官前对照练习:同一份「聊天请求」配置,分别用裸 dict、dataclass、Pydantic 走一遍,并故意传入错误温度。你会清楚感受到:类型注解不是纸面游戏,而是把数据形状抬到与框架请求体 / 工具 schema 同一套操作面的关键一步——也是专栏二工程化与服务 / Agent 对接的入场券之一。
ass、Pydantic
