pydantic

予早 2025-08-31 14:59:18
Categories: Tags:

Pydantic

https://docs.pydantic.dev/latest

Pydantic is the most widely used data validation library for Python.

Pydantic名称由来,python和pedantic的合成词,pedantic意为【过分拘泥于细节(或传统)的;学究气的;迂腐的】。
https://docs.pydantic.dev/latest/#why-use-pydantic
为什么使用Pydantic?验证和转换,https://docs.pydantic.dev/latest/why/

https://docs.pydantic.dev/latest/version-policy/
如何安装Pydantic?
pip install pydantic

Pydantic仅仅依赖:

Pycharm 插件

https://docs.pydantic.dev/latest/integrations/pycharm/

https://docs.pydantic.dev/latest/concepts/models/

类型验证

from typing import List

from pydantic import BaseModel, Field


class ModelAndFieldV2(BaseModel):



    ### 列表验证

    # 列表长度验证
    items: List[str] = Field(..., min_length=1, max_length=10)

    # 列表元素唯一性验证
    tags: List[str] = Field(...)

    # 价格必须为正数的列表
    prices: List[float] = Field(..., gt=0)
    
    
    
# None,type(None)或Literal[None]只允许None值
# bool 布尔类型
# int 整数类型
# float 浮点数类型
# str 字符串类型
# bytes 字节类型
# list 允许list,tuple,set,frozenset,deque, 或生成器并转换为列表
# tuple 允许list,tuple,set,frozenset,deque, 或生成器并转换为元组
# dict 字典类型
# set 允许list,tuple,set,frozenset,deque, 或生成器和转换为集合;
# frozenset 允许list,tuple,set,frozenset,deque, 或生成器和强制转换为冻结集
# deque 允许list,tuple,set,frozenset,deque, 或生成器和强制转换为双端队列
# datetime 的date,datetime,time,timedelta 等日期类型
# typing 中的 Deque, Dict, FrozenSet, List, Optional, Sequence, Set, Tuple, Union,Callable,Pattern等类型
# FilePath,文件路径
# DirectoryPath 目录路径
# EmailStr 电子邮件地址
# NameEmail 有效的电子邮件地址或格式
# PyObject 需要一个字符串并加载可在该虚线路径中导入的 python 对象;
# Color 颜色类型
# AnyUrl 任意网址
# SecretStr、SecretBytes 敏感信息,将被格式化为'**********'或''
# Json 类型
# PaymentCardNumber 支付卡类型
# 约束类型,可以使用con*类型函数限制许多常见类型的值
# conlist
# item_type: Type[T]: 列表项的类型
# min_items: int = None: 列表中的最小项目数
# max_items: int = None: 列表中的最大项目数
# conset
# item_type: Type[T]: 设置项目的类型
# min_items: int = None: 集合中的最小项目数
# max_items: int = None: 集合中的最大项目数
# conint
# strict: bool = False: 控制类型强制
# gt: int = None: 强制整数大于设定值
# ge: int = None: 强制整数大于或等于设定值
# lt: int = None: 强制整数小于设定值
# le: int = None: 强制整数小于或等于设定值
# multiple_of: int = None: 强制整数为设定值的倍数
# confloat
# strict: bool = False: 控制类型强制
# gt: float = None: 强制浮点数大于设定值
# ge: float = None: 强制 float 大于或等于设定值
# lt: float = None: 强制浮点数小于设定值
# le: float = None: 强制 float 小于或等于设定值
# multiple_of: float = None: 强制 float 为设定值的倍数
# condecimal
# gt: Decimal = None: 强制十进制大于设定值
# ge: Decimal = None: 强制十进制大于或等于设定值
# lt: Decimal = None: 强制十进制小于设定值
# le: Decimal = None: 强制十进制小于或等于设定值
# max_digits: int = None: 小数点内的最大位数。它不包括小数点前的零或尾随的十进制零
# decimal_places: int = None: 允许的最大小数位数。它不包括尾随十进制零
# multiple_of: Decimal = None: 强制十进制为设定值的倍数
# constr
# strip_whitespace: bool = False: 删除前尾空格
# to_lower: bool = False: 将所有字符转为小写
# strict: bool = False: 控制类型强制
# min_length: int = None: 字符串的最小长度
# max_length: int = None: 字符串的最大长度
# curtail_length: int = None: 当字符串长度超过设定值时,将字符串长度缩小到设定值
# regex: str = None: 正则表达式来验证字符串
# conbytes
# strip_whitespace: bool = False: 删除前尾空格
# to_lower: bool = False: 将所有字符转为小写
# min_length: int = None: 字节串的最小长度
# max_length: int = None: 字节串的最大长度
# 严格类型,您可以使用StrictStr,StrictBytes,StrictInt,StrictFloat,和StrictBool类型,以防止强制兼容类型



枚举验证
from enum import Enum

from pydantic import BaseModel, Field


class ModeEnum(int, Enum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3
    OTHER = 3


class Model(BaseModel):
    priority: ModeEnum = Field(...)


# 若枚举中有重复值,则生成的枚举是第一个匹配的值
m = Model(priority=3)
print(m)
print(type(m.model_dump(mode='python')))

print(Model(priority=4))

# 优点是pydantic会自动从从枚举中校验值
# 缺点是会把int转为枚举类型,使用的时候可能会造成不方便


# 此时就需要 print(type(m.model_dump(mode='json'))) 进行转换 而不是 python 模式

验证器装饰器

from pydantic import BaseModel, model_validator


class MyModel(BaseModel):
    field_a: str = None
    field_b: str = None
    field_c: str = None

    @model_validator(mode="after")
    def only_one_field_set(self):
        if (n := sum(map(lambda x: int(x is not None), [self.field_a, self.field_b, self.field_c]))) != 1:
            raise ValueError(f'Only one of field_a, field_b, field_c can be set but got {n}')
        return self


model = MyModel(field_a='value_a', field_b='value_b')
from typing import Any

from pydantic import BaseModel, ValidationError, model_validator, field_validator
from pydantic.v1.class_validators import ValidatorCallable
from pydantic_core.core_schema import ValidatorFunctionWrapHandler


class MyModel(BaseModel):
    field_a: str = None
    field_b: str = None
    field_c: str = None

    @model_validator(mode='wrap')
    @classmethod
    def model_validator_wrap(cls, data: Any, handler):
        try:
            print("model_validator_wrap")
            return handler(data)
        except ValidationError:
            raise

    @model_validator(mode="before")
    @classmethod
    def model_validator_before(cls, data: Any):
        print("model_validator_before")
        return data

    @model_validator(mode="after")
    def model_validator_after(self):
        print("model_validator_after")
        return self

    @field_validator('field_a', mode='wrap')
    @classmethod
    def field_validator_wrap_a(cls, value: Any, handler: ValidatorFunctionWrapHandler) -> str:
        try:
            print("field_validator_wrap_a")
            return handler(value)
        except ValidationError:
            raise

    @field_validator('field_a', mode='before')
    @classmethod
    def field_validator_before_a(cls, value: Any) -> Any:
        print("field_validator_before_a")
        return value

    @field_validator('field_a', mode='after')
    @classmethod
    def field_validator_after_a(cls, value: int) -> int:
        print("field_validator_after_a")
        return value

    @field_validator('field_b', mode='wrap')
    @classmethod
    def field_validator_wrap_b(cls, value: Any, handler: ValidatorFunctionWrapHandler) -> str:
        try:
            print("field_validator_wrap_b")
            return handler(value)
        except ValidationError:
            raise

    @field_validator('field_b', mode='before')
    @classmethod
    def field_validator_before_b(cls, value: Any) -> Any:
        print("field_validator_before_b")
        return value

    @field_validator('field_b', mode='plain')
    @classmethod
    def field_validator_plain_b(cls, value: Any) -> Any:
        print("field_validator_plain_b")
        return value

    @field_validator('field_b', mode='after')
    @classmethod
    def field_validator_after_b(cls, value: int) -> int:
        print("field_validator_after_b")
        return value


model = MyModel(field_a='value_a', field_b='value_b', field_c='value_c')

# model_validator_wrap
# model_validator_before
# field_validator_before_a
# field_validator_wrap_a
# field_validator_after_a
# field_validator_plain_b
# field_validator_after_b
# model_validator_after


"""
https://docs.pydantic.dev/latest/concepts/validators/
https://docs.pydantic.org.cn/latest/api/fields/


1.10 中文文档 https://hellowac.github.io/pydantic-zh-cn/v1.10.7-zh-cn/
2版本中文文档 https://docs.pydantic.org.cn/latest/
如上:
- 对于字段:wrap > before > plain > after
- 对于模型:wrap > before > after
- 两者混合 wrap > before > (字段级别) > after
"""

常用自定义验证

import datetime

# 在 v2 中需要显式使用v1相关 API 以进行兼容
from pydantic.v1 import BaseModel, validator
from pydantic_core import ValidationError


class DateModel(BaseModel):
    date: str

    @validator('date', pre=True)
    def parse_date(cls, value):
        try:
            # 尝试将字符串解析为日期,格式为 YYYY-MM-DD
            return datetime.datetime.strptime(value, '%Y-%m-%d').date()
        except ValueError:
            # 如果解析失败,抛出错误
            raise ValueError('Invalid date format. Expected YYYY-MM-DD.')

    @validator('date')
    def check_date(cls, value):
        # 进一步检查日期是否有效
        if not isinstance(value, datetime.date):
            raise ValueError('date must be a valid date')
        return value


# 使用模型进行验证
try:
    date_model = DateModel(date='2023-01-01')
    print(date_model)
except ValidationError as e:
    print(e)

# 尝试一个无效的日期格式
try:
    date_model = DateModel(date='01-01-2023')
except ValidationError as e:
    print(e)

json dict 序列化相关

from datetime import datetime
from typing import Optional, List, Annotated

from pydantic import BaseModel, Field, field_validator, PositiveInt, constr, StringConstraints


class Model(BaseModel):
    # 不要使用 constr 而是 Annotated 和 StringConstraints,因为 constr 是动态构造类型,不利于静态类型检查
    c: constr(strip_whitespace=True, to_upper=True, pattern=r'^[A-Z]+$')
    d: Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, pattern=r'^[A-Z]+$')]
    name1: str | None = Field(None, min_length=1, max_length=50)
    name2: str | None = Field(None, min_length=1, max_length=50)
    operation_type: Optional[str] = Field(..., pattern='^(Bind|UnBind)$', title='操作类型')
    account_ids: List[str] = Field(..., title='账户id列表')
    signup_ts: datetime | None
    date: str = Field(...)
    tastes: dict[str, PositiveInt]

    @field_validator("date")
    @classmethod
    def validate_date(cls, v):
        try:
            datetime.strptime(v, '%Y-%m-%d')
        except Exception:
            raise ValueError("时间字符串不合法")

    @field_validator('account_ids')
    @classmethod
    def validate_accounts(cls, accounts):
        if len(accounts) > 50:
            raise ValueError('提交账户操作数量超过限制')
        for account in accounts:
            if len(account) > 50:
                raise ValueError('账户长度过长')
        return accounts


instance = Model(
    c="CCC",
    d='DDD',
    name1=None,
    operation_type='Bind',
    account_ids=list(map(str, range(1, 51))),
    date="2024-10-12",
    signup_ts='2019-06-01 12:22',
    tastes={
        'wine': 9,
        b'cheese': 7,
        'cabbage': '1',
    },
)

# 返回实际入参提供的字段
print(instance.model_fields_set)

# 转为字典
print(instance.model_dump(mode='python'))

print(instance.model_dump(mode='json'))

# exclude unset 适用于保存到数据库,这里包含默认值和主动设置的空值以及主动设置的非空值
print(instance.model_dump(exclude_unset=True))

print(instance.model_dump(exclude_defaults=True))
print(instance.model_dump(exclude_none=True))

# 转为JSON字符串

print(instance.model_dump_json())

# 将schema导出,以JSON字符串形式
print(instance.model_json_schema())

配置文件支持

# 如果您创建一个继承自BaseSettings的模型,模型初始化程序将尝试通过从环境中读取,来确定未作为关键字参数传递的任何字段的值
# 1. 先从环境变量读取,有就取改值,结束
# 2. 再从配置文件读取,有就取该值,结束
# 3. 最后还没有就取默认值,结束