Course Builder个人学习工作台 · 本地模式
Modern Python:面向有经验开发者的回归路线

Pydantic V2:把外部数据变成可信模型

目标:能够用 Pydantic V2 定义外部输入边界,并有意决定类型转换、严格字段、未知字段和序列化策略。

类型标注只提供静态契约;HTTP JSON、配置和消息队列数据在运行时仍是不可信值。Pydantic BaseModel 读取标注,执行解析与验证,成功后保证模型字段符合声明类型,并能生成 JSON Schema。

默认模式会进行有用的 coercion,例如把 "123" 转成整数 123。但转换是否安全是业务决策:配置文件中的端口字符串通常可以接受,机器 API 的身份 ID 或权限字段可能应该严格拒绝。

安装与完整实验

在项目中运行:

uv add "pydantic[email]"

建立 src/textlab/schemas.py:

from __future__ import annotations

from pydantic import BaseModel, ConfigDict, Field, ValidationError


class MessageInput(BaseModel):
    model_config = ConfigDict(extra="forbid")

    text: str = Field(min_length=1, max_length=5000)
    priority: int = Field(default=0, ge=0, le=10, strict=True)
    tags: list[str] = Field(default_factory=list)


class MessageOutput(BaseModel):
    text: str
    category: str
    priority: int


def demo() -> None:
    message = MessageInput.model_validate(
        {"text": "hello", "priority": 2, "tags": ["demo"]}
    )
    print(message.model_dump())

    try:
        MessageInput.model_validate(
            {"text": "hello", "priority": "2", "unknown": True}
        )
    except ValidationError as exc:
        print([(error["loc"], error["type"]) for error in exc.errors()])


if __name__ == "__main__":
    demo()

运行:

uv run python -m textlab.schemas

预期输出的稳定结构:

{'text': 'hello', 'priority': 2, 'tags': ['demo']}
[(('priority',), 'int_type'), (('unknown',), 'extra_forbidden')]

错误顺序在这里通常稳定,但测试时更适合按 loc/type 集合断言。priority 使用 strict,因此字符串不能悄悄转换;extra="forbid" 能抓住客户端把 priority 拼错一类问题。

Pydantic 与 dataclass 的边界

  • Pydantic model:外部边界、验证、schema、JSON 序列化。
  • dataclass:内部可信值对象、领域行为、较少框架耦合。

小项目可以直接在内部使用 Pydantic model;边界复杂或领域模型稳定时,可验证后转换为第 5 课的 frozen dataclass。不要为了“纯粹”而重复两套完全相同的模型。

Python 输入与 JSON 输入

model_validate() 接 Python 对象;model_validate_json() 直接接 JSON str/bytes。strict 模式下,两种来源对日期等类型可能有不同允许范围,因为 JSON 本身没有 date 类型。测试你的真实入口,不要只测构造器。

引导实验:观察默认 extra 行为

暂时删除 ConfigDict(extra="forbid"),传入 {"text": "hello", "pritorty": 9}。默认 extra 是 ignore,拼错字段会消失,priority 回到 0。恢复 forbid 后错误被及时发现。

这说明“更宽松”并不总是更友好;机器契约中,静默忽略常比明确失败更难排查。

独立练习

新增 nested Metadata 模型,包含 source: str 和 request_id: str | None;让 MessageInput 必须包含 metadata,并写测试覆盖缺失 source、额外字段和合法输入。

参考实现与测试
class Metadata(BaseModel):
    model_config = ConfigDict(extra="forbid")

    source: str = Field(min_length=1)
    request_id: str | None = None


class MessageInput(BaseModel):
    model_config = ConfigDict(extra="forbid")

    text: str = Field(min_length=1, max_length=5000)
    priority: int = Field(default=0, ge=0, le=10, strict=True)
    tags: list[str] = Field(default_factory=list)
    metadata: Metadata
from pydantic import ValidationError

try:
    MessageInput.model_validate({"text": "x", "metadata": {}})
except ValidationError as exc:
    assert exc.errors()[0]["loc"] == ("metadata", "source")
else:
    raise AssertionError("source should be required")

排错路线

  • 以为 Optional 自动代表可省略:字段是否 required 取决于是否有默认值;str | None 允许 null,不必然允许缺失。
  • 错误被静默忽略:检查 extra 配置。
  • strict 开启后旧调用大量失败:审计调用来源并分阶段迁移,不要直接删除测试。

参考资料