本节目标:理解静态类型与运行时数据的边界,掌握 Pydantic V2 的模型定义、字段约束、校验器、序列化与配置读取。
适用版本:Python 3.12+(实测 3.14.6)
9.3 运行时校验与 Pydantic 入门
9.1 与 9.2 两节我们一直在给「代码本身」加类型。但程序还要面对一个静态检查器完全够不着的世界:来自 HTTP 请求的 JSON、来自操作系统的环境变量、来自用户填写的表单。这些数据的类型对不对,只有在运行时才知道。本节就补上这一环。
9.3.1 静态类型管不到运行时数据
回忆 9.1 节的核心结论:类型注解在运行时不做任何检查。看这段代码:
import json
def load_user(raw: str) -> dict[str, int]:
return json.loads(raw) # 检查器无法知道 raw 里到底是什么
json.loads 的返回类型是 Any,会一路关闭检查(9.1 节讲过)。哪怕签名里写 dict[str, int],实际返回的也可能是 {"age": "abc"}。静态类型描述的是「你希望数据是什么样」,而校验解决的是「数据实际是不是那样」——这两件事必须分开做。
9.3.2 dataclass 的类型注解不校验
很多初学者以为 @dataclass 会检查类型,其实不会——它只是帮你生成 __init__ 和 __repr__:
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
u = User(name="Alice", age="not a number") # 完全不报错
print(u, "|", repr(u.age))
User(name='Alice', age='not a number') | 'not a number'
age 明明是 int 注解,却塞进了字符串,运行时毫无反应。dataclass 是「数据容器」,不是「校验器」。要校验,得请 Pydantic 出场。
9.3.3 Pydantic 快速上手:BaseModel
Pydantic 是 Python 生态里最流行的数据校验库,V2 版本用 Rust 重写了核心,速度快了一个数量级。安装(版本号来自 PyPI 实测):
pip install "pydantic==2.13.5"
定义模型就是继承 BaseModel,字段用注解声明:
from pydantic import BaseModel, ValidationError
class User(BaseModel):
name: str
age: int
print(User(name="Alice", age=30))
u2 = User(name="Bob", age="42") # 字符串 "42" 会自动转成 int
print(u2, "| age type:", type(u2.age).__name__)
name='Alice' age=30
name='Bob' age=42 | age type: int
Pydantic 会做「宽松转换」:"42" 能变成 42,这是它和静态检查器最大的不同——愿意帮你转,转不了才报错:
try:
User(name="Carol", age="not-a-number")
except ValidationError as e:
print(str(e))
1 validation error for User
age
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='not-a-number', input_type=str]
For further information visit https://errors.pydantic.dev/2.14/v/int_parsing
错误信息会精确告诉你:哪个字段、什么错误类型、原始输入是什么。这比手写 if not isinstance(...) 强太多。
9.3.4 字段约束:Field
光有类型还不够,业务上常常要求「价格必须大于 0」「名字不能为空」。用 Field 加约束:
from pydantic import BaseModel, Field
class Product(BaseModel):
name: str = Field(min_length=1, max_length=50)
price: float = Field(gt=0)
quantity: int = Field(default=1, ge=1, le=999)
print(Product(name="Keyboard", price=199.0))
name='Keyboard' price=199.0 quantity=1
故意违反约束,Product(name="", price=-5, quantity=0) 会一次性报出全部问题:
3 validation errors for Product
name
String should have at least 1 character [type=string_too_short, input_value='', input_type=str]
For further information visit https://errors.pydantic.dev/2.14/v/string_too_short
price
Input should be greater than 0 [type=greater_than, input_value=-5, input_type=int]
For further information visit https://errors.pydantic.dev/2.14/v/greater_than
quantity
Input should be greater than or equal to 1 [type=greater_than_equal, input_value=0, input_type=int]
For further information visit https://errors.pydantic.dev/2.14/v/greater_than_equal
常用约束一览:
| 参数 | 适用类型 | 含义 |
|---|---|---|
gt / ge | 数值 | 大于 / 大于等于 |
lt / le | 数值 | 小于 / 小于等于 |
min_length / max_length | 字符串、列表 | 长度下限 / 上限 |
pattern | 字符串 | 正则匹配 |
default | 任意 | 默认值 |
9.3.5 嵌套模型与 model_validate / model_dump
真实数据往往是嵌套的,Pydantic 模型可以直接嵌套:
from pydantic import BaseModel
class Address(BaseModel):
city: str
zipcode: str
class Person(BaseModel):
name: str
age: int
address: Address
data = {
"name": "Dana",
"age": "28",
"address": {"city": "Shanghai", "zipcode": "200000"},
}
p = Person.model_validate(data)
print(p)
print(p.model_dump())
print(p.model_dump_json())
name='Dana' age=28 address=Address(city='Shanghai', zipcode='200000')
{'name': 'Dana', 'age': 28, 'address': {'city': 'Shanghai', 'zipcode': '200000'}}
{"name":"Dana","age":28,"address":{"city":"Shanghai","zipcode":"200000"}}
三个关键方法:
model_validate(data):从 dict 校验并构造模型(边界数据的入口)。model_dump():导出为 dict(可mode="json"得到纯 JSON 兼容类型)。model_dump_json():直接导出 JSON 字符串。
age 从字符串 "28" 被转成整数 28,嵌套的 address 也被递归校验成 Address 实例。
9.3.6 自定义校验器:field_validator / model_validator
内置约束不够用时,写自己的校验逻辑。字段级用 field_validator,整模型级用 model_validator:
from pydantic import BaseModel, field_validator, model_validator
from typing import Self
class Signup(BaseModel):
username: str
password: str
confirm: str
@field_validator("username")
@classmethod
def username_lower(cls, v: str) -> str:
return v.strip().lower()
@model_validator(mode="after")
def check_passwords(self) -> Self:
if self.password != self.confirm:
raise ValueError("两次密码不一致")
return self
print(Signup(username=" Alice ", password="a1", confirm="a1"))
username='alice' password='a1' confirm='a1'
username 被自动去空格并转小写。当两次密码不一致时,会抛 value_error:
1 validation error for Signup
Value error, 两次密码不一致 [type=value_error, input_value={'username': 'bob', 'pass...: 'a1', 'confirm': 'a2'}, input_type=dict]
model_validator(mode="after") 在字段都校验完之后运行,能访问整个模型,适合做「跨字段」检查(如密码确认、起始日期早于结束日期)。返回值用 Self(9.2 节讲过)能保持类型正确。
9.3.7 TypeAdapter:校验裸类型
有时你只想校验一个列表、一个联合类型,不想为它建模型。用 TypeAdapter:
from pydantic import TypeAdapter, ValidationError
from typing import Optional
IntList = TypeAdapter(list[int])
print(IntList.validate_python([1, 2, 3]))
print(IntList.validate_json("[4, 5, 6]"))
try:
IntList.validate_python([1, "x", 3])
except ValidationError as e:
print(str(e).splitlines()[2])
MaybeInt = TypeAdapter(Optional[int])
print(MaybeInt.validate_python(None), MaybeInt.validate_python("7"))
[1, 2, 3]
[4, 5, 6]
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='x', input_type=str]
None 7
TypeAdapter 把「类型」本身包装成一个可复用的校验器,validate_json 还能直接吃 JSON 字符串,解析「一个数组接口」时非常顺手。
9.3.8 Pydantic 与静态类型检查器的配合
Pydantic 模型同时也是普通类,检查器完全认识它:给 u.age 赋一个字符串,pyright 会报 Cannot assign to attribute "age",mypy 也会报赋值类型不兼容。默认情况下 Pydantic 允许在实例上赋值,但不会在赋值时重新校验;若希望「赋值也校验」,加上 ConfigDict:
from pydantic import BaseModel, ConfigDict
class StrictUser(BaseModel):
model_config = ConfigDict(validate_assignment=True)
age: int
这样 u.age = "abc" 会抛 ValidationError。ConfigDict 里还有 extra="forbid"(禁止多余字段)、frozen=True(不可变)等常用开关。静态检查 + 运行时校验双管齐下,才是 Pydantic 的正确用法。
9.3.9 用 pydantic-settings 读环境变量
配置通常来自环境变量,它们全都是字符串。pydantic-settings 把环境变量映射成带类型的配置对象:
import os
from pydantic_settings import BaseSettings, SettingsConfigDict
class AppSettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="APP_")
debug: bool = False
db_url: str = "sqlite:///local.db"
pool_size: int = 5
os.environ["APP_DEBUG"] = "true"
os.environ["APP_POOL_SIZE"] = "10"
print(AppSettings())
debug=True db_url='sqlite:///local.db' pool_size=10
"true" 被转成 True,"10" 被转成 10,env_prefix="APP_" 自动加前缀。安装用 pip install "pydantic-settings==2.15.0"。
这是 FastAPI 项目读取配置的标准做法。更完整的用法(.env 文件、密钥管理、多环境切换)会在第 16 章 Web 开发与实战书中展开,本节点到为止。
9.3.10 JSON 边界数据的处理流程
把本节串起来,处理一份外部 JSON 的推荐流程是:
- 拿到原始字符串(来自网络、文件、消息队列)。
- 用
model_validate_json()或model_validate()解析——校验发生在这一步。 - 捕获
ValidationError,把错误转成用户可读的提示或日志。 - 在程序内部只传递已校验的模型实例,不再传裸 dict。
- 出口处用
model_dump_json()序列化回 JSON。
from pydantic import BaseModel, ValidationError
class Order(BaseModel):
order_id: str
amount: float
raw = '{"order_id": "A-100", "amount": 9.9}'
try:
order = Order.model_validate_json(raw)
print("OK:", order.model_dump())
except ValidationError as e:
print("校验失败:", e.error_count(), "处")
OK: {'order_id': 'A-100', 'amount': 9.9}
核心思想:让「已校验的模型」成为程序内部的统一货币。边界处严格校验,内部就再也不用担心类型问题了。
小结
- 静态类型管不到运行时数据:
json.loads返回Any,@dataclass也不校验类型。 - Pydantic V2(
pydantic==2.13.5)用BaseModel定义模型,会做宽松类型转换,失败时抛信息丰富的ValidationError;Field加约束,field_validator/model_validator写自定义校验。 model_validate/model_dump/model_dump_json负责边界进出,TypeAdapter校验裸类型;ConfigDict(validate_assignment=True)让赋值也校验。pydantic-settings把环境变量映射成带类型的配置,是 FastAPI 项目的标准配置方案。
到这里,第 9 章「类型注解」就完整了:9.1 讲语法与工具,9.2 讲复杂类型结构,9.3 讲运行时校验。下一章我们回到字符串与文件,学习字符串处理与正则——这是文本类任务的必备技能。
阅读导航:上一节:泛型、Protocol、TypedDict 与 PEP 695 · 下一节:字符串处理与正则 re 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。