小张盯着屏幕上那一行报错,已经发了十分钟的呆。
事情是这样的。他们团队在做一个用户注册功能,前端传过来一份JSON数据,按理说应该有用户名、邮箱、年龄三个字段。结果测试同学随手填了个年龄“二十五”,程序直接炸了——类型错误,字符串不能和整数比较。
小张翻了翻代码,发现校验逻辑散落在各处:views.py里有一堆if判断,models.py里又有几个正则表达式,utils.py里还藏着个专门清洗数据的函数。改一个地方,另外两个地方就忘了同步。
他叹了口气。这种问题,在这个项目里已经出现过无数次了。
如果你也写过Python后端,你一定懂这种感觉:数据校验这件事,做起来不难,但做好很难。你永远不知道用户会传什么乱七八糟的东西进来。今天是个字符串年龄,明天可能就是空的邮箱,后天直接少传一个字段。
而且最烦的是,校验代码写多了,业务逻辑反而看不清楚。一个函数里,前面二十行都在做类型检查和判空,真正干活的代码被挤到最后几行。
我后来才知道,这个问题早就有了解法。它的名字叫Pydantic。
一、从一个最简单的例子说起
先别急着看那些复杂的文档。Pydantic最核心的东西,其实特别好理解。
假设你现在要写一个用户注册的接口。传统写法大概是这样:
def register_user(data):
if not data.get('username'):
raise ValueError('用户名不能为空')
if not isinstance(data.get('age'), int):
raise ValueError('年龄必须是数字')
if data.get('age') < 18:
raise ValueError('年龄必须大于18岁')
# ... 继续校验邮箱、手机号等等
# 校验通过后,才真正开始处理业务逻辑
这段代码的问题不是它错了,而是它把校验和业务逻辑混在一起。看代码的人需要一边理解校验规则,一边理解业务逻辑,脑子很累。
用Pydantic改一下:
from pydantic import BaseModel, Field
class User(BaseModel):
username: str
age: int = Field(ge=18)
email: str
就这些。没了。
当你需要校验数据的时候:
def register_user(data):
user = User(**data)
# 校验已经自动完成,这里只管业务逻辑
save_to_database(user)
如果数据不符合要求,Pydantic会自动抛出ValidationError,并且告诉你哪里错了。比如age传了字符串"二十五",它会说"Input should be a valid integer"。如果age传了16,它会说"Input should be greater than or equal to 18"。
这就是Pydantic最核心的价值:把校验规则从业务代码里抽离出来,让代码更干净,让错误信息更清晰。
二、自动类型转换,帮你省掉无数if语句
你有没有遇到过这种场景:前端传过来的JSON里,所有数字都是字符串。你拿到数据之后,得挨个转成整数或浮点数,不然没办法做数值计算。
在Pydantic里,这事是自动的。
from pydantic import BaseModel
class Product(BaseModel):
price: float
quantity: int
data = {"price": "19.99", "quantity": "3"}
product = Product(**data)
print(product.price) # 19.99,已经是float
print(product.quantity) # 3,已经是int
只要字符串的内容能安全地转换成目标类型,Pydantic就帮你自动完成。转换不了的时候,才会报错。
这个特性在对接API的时候尤其好用。你不需要再写一行一行的int(data['age']),也不需要担心哪个字段忘记转了。
三、可选字段和默认值,处理不完整数据
真实世界的数据很少是完整的。用户可能不填手机号,API可能不返回某个字段。Pydantic处理这种情况也很简单:
from pydantic import BaseModel
from typing import Optional
class UserProfile(BaseModel):
username: str
age: int
phone: Optional[str] = None
is_active: bool = True
Optional[str] = None 表示phone字段可以不存在,如果不存在就设为None。is_active = True 表示这个字段有默认值,调用方可以不传。
这样,当你接收到不完整的数据时,Pydantic会自动补全缺失的字段,而不是直接报错。
data = {"username": "张三", "age": 25}
profile = UserProfile(**data)
print(profile.phone) # None
print(profile.is_active) # True
这在实际开发中非常实用。你不需要写一堆data.get('phone', None)这样的代码,模型定义本身就是文档。
四、Field约束,让校验规则一目了然
刚才我们用ge=18限制了年龄必须大于等于18。Field还提供了很多其他约束:
from pydantic import BaseModel, Field
class Product(BaseModel):
name: str = Field(min_length=1, max_length=100)
price: float = Field(gt=0, description="价格必须大于0")
rating: int = Field(ge=1, le=5)
tags: list[str] = Field(max_items=10)
这些约束写在一起,比散落在各个地方的if语句好维护多了。你想改某个字段的校验规则,只需要改模型定义那一行,不用在整个代码库里到处搜。
而且这些约束不只是运行时生效,还能自动生成API文档。如果你用的是FastAPI,这些约束会自动映射到OpenAPI文档里,前端的人看一眼就知道该怎么传参数。
五、自定义校验器,处理那些复杂逻辑
有些校验规则不是简单的大小比较能搞定的。比如手机号格式、密码强度、两个字段之间的依赖关系。
这时候可以用@field_validator:
from pydantic import BaseModel, field_validator
import re
class Account(BaseModel):
username: str
password: str
confirm_password: str
@field_validator('username')
def username_alphanumeric(cls, v):
if not v.isalnum():
raise ValueError('用户名只能包含字母和数字')
if len(v) < 3:
raise ValueError('用户名至少3个字符')
return v.lower() # 可以顺便做规范化
@field_validator('confirm_password')
def passwords_match(cls, v, info):
if v != info.data.get('password'):
raise ValueError('两次输入的密码不一致')
return v
注意第二个校验器,它用到了info.data来获取其他字段的值。这让你可以校验字段之间的依赖关系。
自定义校验器里还能做数据清洗。比如用户名统一转小写,电话号码去掉横线和空格。这样后面用到这些数据的时候,已经是最干净的状态了。
六、嵌套模型,处理复杂数据结构
现实中的数据往往是嵌套的。一个订单包含多个商品,每个商品又有自己的属性。Pydantic处理这种嵌套非常自然:
from pydantic import BaseModel
from typing import List
class Address(BaseModel):
street: str
city: str
zip_code: str
class OrderItem(BaseModel):
product_id: int
quantity: int
price: float
class Order(BaseModel):
order_id: str
address: Address
items: List[OrderItem]
total: float
当你传入嵌套的数据结构时,Pydantic会递归地校验每一层:
order_data = {
"order_id": "ORD-001",
"address": {"street": "123 Main St", "city": "Beijing", "zip_code": "100000"},
"items": [
{"product_id": 1, "quantity": 2, "price": 19.99},
{"product_id": 2, "quantity": 1, "price": 49.99}
],
"total": 89.97
}
order = Order(**order_data)
如果某个商品少了quantity字段,或者address里少了city,Pydantic会在对应的层级报错,告诉你具体是哪个位置出了问题。排查起来非常方便。
七、处理真实API响应的技巧
在实际工作中,你经常要处理各种API返回的数据。有些API返回的字段名和你代码里用的不一样,有些API返回的日期格式很奇怪。
Pydantic提供了field_alias和自定义校验器来解决这些问题:
from pydantic import BaseModel, Field, field_validator
from datetime import datetime
class APIResponse(BaseModel):
user_id: int = Field(alias="id")
full_name: str = Field(alias="name")
created_at: datetime
@field_validator('created_at', mode='before')
def parse_date(cls, v):
# 处理各种奇怪的日期格式
if isinstance(v, str):
return v.replace('Z', '+00:00')
return v
alias让你可以用API返回的字段名,但代码里用自己习惯的名字。mode='before'的校验器在类型转换之前运行,最适合处理那些格式不统一的数据。
这样,不管外部数据有多乱,到了你的业务代码里,都是规规矩矩的Python对象。
八、性能怎么样?
有人可能会担心:加了这么多校验,会不会变慢?
Pydantic的核心校验逻辑是用Rust写的(pydantic-core),比纯Python实现快很多。官方文档说,Pydantic V2比V1快了大约17倍。
在实际项目中,校验的开销通常远小于数据库查询或网络请求的开销。所以放心用,它不是瓶颈。
九、写在最后
小张后来把项目里的数据校验全部重构成了Pydantic模型。原来散落在各个文件里的校验代码,被几十行模型定义替代了。代码量减少了,可读性提高了,bug也少了。
有一次新来的同事接手他的代码,看完模型定义之后说:“原来这个字段有这些限制,一看就懂了。”
这就是Pydantic最大的价值——它让数据校验这件事,从“到处贴胶布”变成了“一次性定义清楚”。
数据校验不该是代码里的噪音,它应该是代码的一部分,清晰、简洁、可维护。
如果你还在手动写一堆if语句做校验,不妨试试Pydantic。你会回来感谢它的。