Python技术迷

你以为你会用 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 才不会越来越像垃圾回收站。 这玩意用顺手了,代码会省很多解释;用不顺,校验层本身就会变成新问题。