你以为你会用 Pydantic?这11条最佳实践让你少走弯路
接口明明收的是 age: 18,结果线上日志里混进来一个 "18",再过两层,没人知道它到底是字符串还是整数。 这种问题,我一般不先怪前端,先看模型怎么收的。很多人用 Pydantic,只停留在“能校验”这一步,真到项目里,坑基本都在边边角角。
Pydantic 不是把字段一摆就完事了,写法差一点,后面排错会很烦。
先看第一类坑:拿它当字典校验器,不当边界模型用。 请求进来、配置读进来、消息消费进来,这些地方最该上 Pydantic。别等数据已经在业务里跑半天了,才想起来补校验。
from pydantic import BaseModel, EmailStr
classCreateUserReq(BaseModel):
name: str
age: int
email: EmailStr
这种模型,应该卡在入口,而不是 service 里再 if age < 0 补锅。
第二,能写约束就别靠注释。注释没人执行,模型会。
from pydantic import BaseModel, Field
classItemQuery(BaseModel):
page: int = Field(1, ge=1)
size: int = Field(20, ge=1, le=100)
keyword: str = Field("", max_length=50)
第三,默认值别乱给,尤其是可变对象。这类问题 Python 老手都见过,Pydantic 一样能踩。
from pydantic import BaseModel, Field
classBatchReq(BaseModel):
ids: list[int] = Field(default_factory=list)
别写成 ids: list[int] = [],这种错低级,但线上真不少。
第四,字段别名要早点定,不然后面接口改名很疼。 外部传 userId,内部想用 user_id,别硬拧。
from pydantic import BaseModel, Field, ConfigDict
classUserInfo(BaseModel):
model_config = ConfigDict(populate_by_name=True)
user_id: int = Field(alias="userId")
nick_name: str = Field(alias="nickName")
第五,别把所有字段都做成可选。 很多人图省事,满屏 str | None = None,最后等于没校验。可选字段应该真可选,不是怕报错才可选。
第六,用 @field_validator 做单字段清洗,别把脏活散在业务代码里。
from pydantic import BaseModel, field_validator
classLoginReq(BaseModel):
username: str
@field_validator("username")
@classmethod
defclean_username(cls, v: str) -> str:
v = v.strip()
ifnot v:
raise ValueError("username不能为空")
return v
像 strip()、大小写统一、手机号格式整理,这种都该在模型里收口。
第七,跨字段判断别写到 service 里兜底,用模型自己兜住。 开始时间晚于结束时间,这种不是业务逻辑,是入参合法性。
from datetime import datetime
from pydantic import BaseModel, model_validator
classExportReq(BaseModel):
start_time: datetime
end_time: datetime
@model_validator(mode="after")
defcheck_time_range(self):
if self.start_time > self.end_time:
raise ValueError("start_time不能大于end_time")
return self
第八,配置项一定用 BaseSettings 管。 环境变量、.env、容器配置,这块我见过太多字符串乱飞,最后是端口都能读错类型。
from pydantic_settings import BaseSettings
classAppSettings(BaseSettings):
app_name: str = "demo"
redis_host: str
redis_port: int = 6379
debug: bool = False
settings = AppSettings()
第九,序列化时别一把梭。 有些字段不该往外吐,比如密码、内部标记、调试信息。导出 JSON 时要有选择。
classUserVO(BaseModel):
id: int
name: str
password_hash: str
u = UserVO(id=1, name="dongge", password_hash="xxx")
print(u.model_dump(exclude={"password_hash"}))
第十,错误信息要留给日志,不要原样甩给用户。 Pydantic 的报错很细,这对排查有用,但不适合直接透出到前端。接口层收异常,日志记全,返回信息收敛一点,别把内部字段结构都暴露出去。
第十一,别在热路径里反复构造大模型。 Pydantic 很方便,但不是没成本。批量处理、日志回放、消息清洗这种场景,模型层级一深,性能开销会很明显。该复用就复用,该拆轻模型就拆,别一个导入脚本套七层嵌套模型,最后慢得像卡住了一样,还怀疑数据库。
再给一段我平时处理脏 JSON 时常用的写法,挺实用:
from pydantic import BaseModel, ValidationError
classOrderMsg(BaseModel):
order_id: int
amount: float
status: str
raw_list = [
{"order_id": 101, "amount": "19.9", "status": "paid"},
{"order_id": "xx", "amount": 30, "status": "paid"},
]
for raw in raw_list:
try:
msg = OrderMsg.model_validate(raw)
print("ok:", msg.model_dump())
except ValidationError as e:
print("bad data:", raw)
print(e.errors())
这段东西放在消息消费前面,比你后面查半天脏数据强得多。
Pydantic 真正有用的地方,不是“会写 BaseModel”,而是你知不知道该把它放在哪一层,知道哪些约束该前置,哪些清洗该收口,哪些字段绝不能放水。
模型写得干净,后面的 service 才不会越来越像垃圾回收站。 这玩意用顺手了,代码会省很多解释;用不顺,校验层本身就会变成新问题。