类型提示是 Python 给动态语言打的"补丁",Pydantic 则是把这张补丁缝进运行时的"铠甲"。本文串联两部分内容:先用类型注解在静态阶段约束代码,再用 Pydantic 在运行时真正校验、转换、序列化数据。
一、背景:为什么 Python 需要类型层
Python 是动态类型语言------变量类型在运行时才确定,解释器不关心你标不标类型。这带来了灵活,也带来了隐患:拼错键名、传错类型、int 与 str 混用,往往要等到运行时才炸。
类型提示(Type Hints,PEP 484,Python 3.5 引入)是一种补救措施 :它不改变程序运行时的行为,只为类型检查器(mypy/pyright)和 IDE 提供额外的类型信息。
而 Pydantic 在此基础上再进一步------它利用类型注解,在运行时真的去校验、转换、序列化数据。两者一静一动,构成现代 Python 工程(尤其是 FastAPI 栈)的类型底座。
一句话定位:
- 类型提示:静态阶段生效,只"提示",不"强制"。
- Pydantic:运行时生效,既"校验"又"转换"。
二、类型提示核心速览
2.1 基础注解
python
# 变量与函数注解
name: str = "Alice"
age: int = 25
def greet(name: str, age: int) -> str:
return f"Hello {name}, you are {age} years old"
# 无返回值要显式标注 None
def log_message(message: str) -> None:
print(f"LOG: {message}")
关键迁移点(Python 3.9+) :直接用内置泛型,不要再 from typing import List。
python
# 现代写法(推荐)
def process_data(items: list[str], mapping: dict[str, int]) -> list[dict[str, int]]:
return [{"result": len(item)} for item in items]
# 传统写法(3.8 及以前才需要)
from typing import List, Dict
def old_style(items: List[str]) -> List[Dict[str, int]]: ...
2.2 特殊类型
| 类型 | 写法 | 说明 |
|---|---|---|
| 可选 | Optional[X] / `X |
None` |
| 联合 | Union[X, Y] / `X |
Y` |
| 字面量 | Literal["a", "b"] |
锁死有限枚举取值 |
| 任意 | Any |
关闭类型检查(慎用) |
| 永不返回 | NoReturn |
只抛异常或死循环 |
Optional 三种写法完全等价,3.10+ 的 | 语法最清爽:
python
def find_user(user_id: int) -> str | None:
return "Alice" if user_id == 1 else None
Literal 是隐藏利器------它比裸 str 安全得多:
python
def http_request(method: Literal["GET", "POST", "PUT", "DELETE"]) -> str: ...
# http_request("PATCH") ← mypy 直接标红,运行时才炸的 bug 提前拦下
2.3 函数类型 Callable
Callable 把"函数本身"当数据类型标注,是高阶函数与回调的静态约束:
python
from typing import Callable
MathOperation = Callable[[int, int], float] # 两 int 入参、float 返回
def apply_operation(a: float, b: float, op: MathOperation) -> float:
return op(a, b)
apply_operation(5, 3, add) # add 是函数对象本身,不是调用结果
注意:Callable 只能标位置参数,表达不了关键字参数/默认值;可选回调要配 Optional[Callable[..., None]]。
2.4 类与自定义类型
前向引用解决递归结构的"自己引用自己"问题:
python
class Node:
def __init__(self, value: int, next_node: 'Node' = None) -> None:
self.value = value
self.next_node = next_node
文件头加 from __future__ import annotations 后,可省略引号直接写 next_node: Node。
泛型 TypeVar 是类型提示里最难也最有用的部分:
python
from typing import TypeVar, Callable
T = TypeVar('T') # 类型占位符,不是具体类
Processor = Callable[[T], T] # 接收 T、返回 T
def process_list(items: list[T], processor: Processor[T]) -> list[T]:
return [processor(item) for item in items]
T 的灵魂在于类型一致性 :一旦实参把 T 推断成 int,函数签名里所有 T 同步锁定为 int,保证进出同类型。bound= 还能把 T 收窄到某家族:
python
T = TypeVar('T', bound=Animal)
def make_sound(a: T) -> str:
return a.speak() # 静态阶段即可调用家族方法
2.5 高级特性
泛型类让容器强制只装一种类型:
python
from typing import TypeVar, Generic
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self.items: list[T] = []
def push(self, item: T) -> None: ...
def pop(self) -> T | None: ...
Stack[int]().push("x") # mypy 报错:str 不能进 Stack[int]
@overload 是"静态障眼法" :@overload 桩只给类型检查器看,运行时被最后一个真正实现覆盖,实现函数类型必须覆盖所有重载并集。Python 无"真重载"(同名函数后定义覆盖先定义),这是体验上的弥补。
2.6 三种数据载体怎么选
| 工具 | 运行时本质 | 访问 | JSON | 适用 |
|---|---|---|---|---|
TypedDict |
普通 dict | [] |
原生 | JSON/字典、无方法 |
dataclass |
真实对象 | . |
需转换 | 纯数据载体 |
传统 class |
真实对象 | . |
需转换 | 封装行为+数据 |
TypedDict 给字典定"蓝图",NotRequired 标可选键,逼你用 .get() 防 KeyError------但它运行时仍是 dict,不校验。
python
from typing import TypedDict, NotRequired
class UserProfile(TypedDict):
name: str
age: int
email: NotRequired[str]
三、Pydantic:把契约缝进运行时
3.1 它解决什么
没有 Pydantic 时,校验靠手写的 if/isinstance 防御性代码,冗长且易漏:
python
def create_user(data: dict):
if "name" not in data:
raise ValueError("name is required")
if not isinstance(data.get("age"), int):
raise ValueError("age must be int")
if data.get("age", 0) < 0:
raise ValueError("age must be positive")
# ... 更多验证
用 Pydantic 后,声明即校验:
python
from pydantic import BaseModel, Field
class User(BaseModel):
name: str
age: int = Field(gt=0) # 必须大于 0
email: str = ""
user = User(name="Alice", age="25") # age 字符串自动转 int
print(type(user.age)) # <class 'int'>
核心优势:声明式验证、自动类型转换("25"→25)、详细结构化报错、序列化能力、v2 Rust 核心性能提升 5-50 倍。
3.2 基础模型与约束
python
from pydantic import BaseModel, Field
class User(BaseModel):
name: str = Field(min_length=2, max_length=50)
age: int = Field(gt=0, le=150) # 大于 0,小于等于 150
phone: str = Field(pattern=r'^1[3-9]\d{9}$') # 正则约束
role: str = Field(default="user", description="用户角色")
嵌套模型自动递归校验并实例化:
python
class Address(BaseModel):
city: str
street: str
class User(BaseModel):
name: str
address: Address
u = User(name="Bob", address={"city": "Beijing", "street": "Zhongguancun"})
print(u.address.city) # Beijing ------ 字典已被实例化为 Address 对象
3.3 自定义验证器
字段级用 @field_validator(必须为 @classmethod),模型级用 @model_validator 做跨字段校验:
python
from pydantic import BaseModel, field_validator, model_validator
class UserCreate(BaseModel):
password: str
confirm_password: str
@field_validator('password')
@classmethod
def validate_password(cls, v: str) -> str:
if len(v) < 8:
raise ValueError('密码至少8位')
return v
@model_validator(mode='after')
def passwords_match(self) -> 'UserCreate':
if self.password != self.confirm_password:
raise ValueError('两次密码输入不一致')
return self
mode='after' 表示等所有字段完成类型转换和单字段校验后再执行,适合比较多个已解析字段;mode='before' 在字段验证前拿到原始输入,适合预处理。
3.4 配置 model_config
python
from pydantic import BaseModel, ConfigDict
class User(BaseModel):
model_config = ConfigDict(
str_strip_whitespace=True, # 去首尾空格
str_to_lower=True, # 转小写
validate_assignment=True, # 赋值时也验证
extra='forbid' # 禁止额外字段
)
name: str
age: int
3.5 序列化与导出
python
user = User(name="Alice", age=25, password="secret123")
user.model_dump() # 转 dict
user.model_dump(exclude={'password'}) # 排除敏感字段
user.model_dump_json() # 转 JSON 字符串
user.model_dump(include={'name', 'age'}) # 只保留指定字段
3.6 与 FastAPI 集成(最核心场景)
python
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class CreateUser(BaseModel):
name: str
email: str
age: int = Field(gt=0, le=150)
class UserResponse(BaseModel):
id: int
name: str
email: str
@app.post("/users", response_model=UserResponse)
def create_user(user: CreateUser):
return {"id": 1, "name": user.name, "email": user.email}
FastAPI 自动完成:解析 JSON → 校验字段 → 失败返回 422(带详细错误)→ 按 response_model 过滤并序列化输出。/docs 自动生成 Swagger 文档。
3.7 环境变量配置
python
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "My App"
database_url: str # 必填,从环境变量 DATABASE_URL 读取
secret_key: str
class Config:
env_file = '.env'
3.8 高级类型开箱即用
EmailStr、HttpUrl、PastDate 等内置类型自动校验格式;配合 Enum、Literal、Union 覆盖大多数业务场景:
python
from pydantic import BaseModel, EmailStr, HttpUrl
from typing import Literal, Union
from enum import Enum
class Role(str, Enum):
ADMIN = "admin"
USER = "user"
class User(BaseModel):
email: EmailStr
website: HttpUrl
role: Role = Role.USER
identifier: Union[int, str]
status: Literal["active", "inactive", "banned"]
3.9 处理验证错误
python
from pydantic import ValidationError
try:
User(name="Alice", age="not_a_number")
except ValidationError as e:
for error in e.errors():
print(f"字段: {error['loc']}, 错误: {error['msg']}")
四、v1 vs v2 关键差异
| 特性 | v1 | v2 |
|---|---|---|
| 核心引擎 | 纯 Python | Rust (pydantic-core) |
| 方法名 | .dict() / .json() |
.model_dump() / .model_dump_json() |
| 配置 | class Config |
model_config = ConfigDict(...) |
| 验证器 | @validator |
@field_validator / @model_validator |
| ORM 桥接 | orm_mode=True |
from_attributes=True |
新项目直接用 v2。
五、完整实战:用户管理系统
下面这段串联了继承复用、字段级/模型级验证、ORM 桥接与响应过滤,是把前文所有概念落地的标准范式:
python
# 文件路径: examples/user_system.py
from pydantic import (
BaseModel, Field, EmailStr,
field_validator, model_validator, ConfigDict
)
from typing import Optional
from datetime import datetime
class UserBase(BaseModel):
"""所有用户模型的基类"""
username: str = Field(min_length=3, max_length=20, pattern=r'^[a-zA-Z0-9_]+$')
email: EmailStr
age: Optional[int] = Field(default=None, ge=0, le=150)
class UserCreate(UserBase):
"""注册模型:接收前端表单,含跨字段校验"""
password: str = Field(min_length=8)
confirm_password: str
@model_validator(mode='after')
def passwords_match(self) -> 'UserCreate':
if self.password != self.confirm_password:
raise ValueError('两次密码输入不一致')
return self
@field_validator('password')
@classmethod
def validate_password(cls, v: str) -> str:
if not any(c.isupper() for c in v):
raise ValueError('密码必须包含大写字母')
return v
class UserInDB(UserBase):
"""数据库模型:含敏感字段,from_attributes 桥接 ORM"""
id: int
hashed_password: str
created_at: datetime
is_active: bool = True
model_config = ConfigDict(from_attributes=True)
class UserResponse(UserBase):
"""响应模型:自动剔除密码等敏感字段"""
id: int
created_at: datetime
model_config = ConfigDict(from_attributes=True)
# 业务逻辑:注册 → 入库 → 响应过滤
new_user = UserCreate(
username="alice_2024", email="alice@example.com", age=25,
password="HelloWorld1", confirm_password="HelloWorld1"
)
user_db = UserInDB(
id=1, username=new_user.username, email=new_user.email,
age=new_user.age, hashed_password="hashed_" + new_user.password,
created_at=datetime.now()
)
response = UserResponse.model_validate(user_db) # 直接吃 ORM 对象
print(response.model_dump_json(indent=2)) # 自动排除敏感字段
三个模型通过继承 UserBase 复用字段;UserCreate 做密码复杂度与一致性校验;UserInDB/UserResponse 用 from_attributes=True 直接从 ORM 对象填充,响应模型天然过滤掉 hashed_password 等内部字段------这正是"用类型分层隔离内外数据"的工程实践。
六、总结
把两部分串起来看,Python 的类型体系是一张分层网:
| 层级 | 角色 |
|---|---|
| 类型注解 | 声明数据结构(静态) |
TypedDict |
静态契约,运行时仍是 dict |
dataclass |
轻量数据载体 |
BaseModel(Pydantic) |
运行时校验 + 转换 + 序列化 |
Field / Validator |
细粒度约束与业务规则 |
model_config |
控制模型行为 |
核心认知:类型提示是"契约"不是"枷锁"------零运行时开销,却把一大类 bug 挡在落码之前;Pydantic 补齐了"运行时真校验"这一环,让数据验证从防御性编程变成声明式编程。
落到 FastAPI + SQLAlchemy 栈里就是清晰分工:JdRecord(ORM class,全字段+表映射)对内,JdRecordOut(Pydantic,精简字段)对外,response_model 自动过滤与序列化。理解这套分层,FastAPI 的很多"魔法"就不再是黑箱。
类型即文档,注解即契约。这是 Pydantic 的设计哲学,也是现代 Python 开发的最佳实践。