1. 简介

1.7.1 版本的文档。

使用Python类型注解进行数据验证和设置管理。

Pydantic 在运行时强制执行类型提示,并在数据无效时提供用户友好的错误信息。

定义数据如何表示为纯粹和规范的 Python ,并使用 pydantic 对其进行验证。

1.1 示例:

from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel


class User(BaseModel):
    id: int
    name = 'John Doe'
    signup_ts: Optional[datetime] = None
    friends: List[int] = []


external_data = {
    'id': '123',
    'signup_ts': '2019-06-01 12:22',
    'friends': [1, 2, '3'],
}
user = User(**external_data)
print(user.id)
#> 123
print(repr(user.signup_ts))
#> datetime.datetime(2019, 6, 1, 12, 22)
print(user.friends)
#> [1, 2, 3]
print(user.dict())
"""
{
    'id': 123,
    'signup_ts': datetime.datetime(2019, 6, 1, 12, 22),
    'friends': [1, 2, 3],
    'name': 'John Doe',
}
"""

这里发生了什么:

  • id 是 int 类型;注释声明告诉pydantic该字段是必须的。如果可能,字符串、字节或浮点数将强制转换为int,否则将引发异常。
  • name 从默认值推断为其为 str 类型,该字段不是必须的,因为它有默认值。
  • signup_ts 是 datetime 类型,该字段不是必须的,默认值为 None。pydantic会将表示unix时间戳(例如1496498400)的 int 类型或表示时间和日期的字符串处理成 datetime 类型。
  • friends 使用Python的 typing 系统,需要一个整数列表,就像 id 字段一样,类整数的对象将会被转换为整数。

如果验证失败,pydantic会抛出一个错误,列出错误的原因:

from pydantic import ValidationError
try:
    User(signup_ts='broken', friends=[1, 2, 'not number'])
except ValidationError as e:
    print(e.json())

输出:

[
  {
    "loc": [
      "id"
    ],
    "msg": "field required",
    "type": "value_error.missing"
  },
  {
    "loc": [
      "signup_ts"
    ],
    "msg": "invalid datetime format",
    "type": "value_error.datetime"
  },
  {
    "loc": [
      "friends",
      2
    ],
    "msg": "value is not a valid integer",
    "type": "type_error.integer"
  }
]

1.2 基本原理

pydantic使用了一些很酷的新语言特性,但我为什么要使用它呢?

  • 与你的IDE/linter/brain配合得很好不需要学习新的模式定义微语言。如果您知道如何使用 Python 的类型提示,也就知道如何使用 pydantic。因为pydantic数据结构只是您使用类型注解定义的类的实例,所以自动完成、linting、mypy、IDE(尤其是 PyCharm)和您的直觉都应该能够正确地处理经过验证的数据。
  • 多用途pydantic的 [BaseSettings](# 3.9 设置管理) 类允许在 “验证此请求数据” 上下文和 “加载我的系统设置” 上下文中使用。主要区别在于,系统设置可以从环境变量读取,并且通常需要更复杂的对象,如DSN和Python对象。
  • 快速pydantic比其他所有测试库都要快。
  • 可以验证复杂结构使用[递归pydantic模型](# 3.1.2 递归模型)、typing 的标准类型 (如 List、Tuple 和 Dict 等) 和验证器,可以很清晰且容易地定义、验证和解析复杂数据模式。
  • 可拓展pydantic允许定义[自定义数据类型](# 3.2.7 自定义数据类型),或者您可以使用被 validator 装饰器装饰的模型上的方法来扩展验证。
  • dataclasses 集成和 BaseModel 一样,pydantic提供了一个 [dataclass](# 3.7 Dataclasses) 装饰器,它创建带有输入数据解析和验证的(几乎)普通的Python数据类。

2. 安装

pip install pydantic

Pydantic除了Python3.6、3.7、3.8 或 3.9(和Python3.6中的 dataclasses 包)之外,不需要其他依赖项。

Pydantic 可以可选的使用 Cython 进行编译,将会带来 30%-50%的性能提升。

PyPI可以为Linux、MacOS和64位Windows提供二进制文件。如果您是手动安装,请在安装pydantic之前安装 cython,这样编译就会自动进行。

要测试pydantic 是否已经编译,可以使用如下方法:

>>> import pydantic
>>> print(pydantic.compiled)

Pydantic 有三个可选依赖:

  • 如果需要 email 验证,可以添加 email-validator。
  • 要在 Python3.8之前的版本中使用 Literal,需要安装 typing-extensions。
  • 使用 Settings 的 dotenv 文件支持需要安装 python-dotenv。

要将这些与 pydantic 一起安装,可以使用如下方式:

pip install pydantic[email]
# or
pip install pydantic[typing_extensions]
# or
pip install pydantic[dotenv]
# or just
pip install pydantic[email,typing_extensions,dotenv]

当然,你可以使用 pip install email-validator 和/或 pip install typing_extensions 手动安装这些依赖。

如果想从存储库直接安装 pydantic,可以使用:

pip install git+git://github.com/samuelcolvin/pydantic@master#egg=pydantic
# or with extras
pip install git+git://github.com/samuelcolvin/pydantic@master#egg=pydantic[email,typing_extensions]

3. 用法详解

3.1 模型

在pydantic中定义对象的主要方法是通过模型(模型只是继承自 BaseModel 的类)。

您可以将模型看作严格类型语言中的类型,或者看作API中单个端点的需求。

不受信任的数据可以传递给模型,在解析和验证之后,pydantic保证结果模型实例的字段将符合模型上定义的字段类型。

注意

pydantic主要是一个解析库,而不是验证库。验证是达到目的的一种手段:构建符合所提供的类型和约束的模型。

换句话说,pydantic保证输出模型的类型和约束,而不是输入数据。

这听起来像是一个深奥的区别,但其实不然。如果您不确定这是什么意思或者它如何影响您的使用,那么您应该阅读下面关于[数据转换](# 3.1.16 数据转换)的部分。

3.1.1 基本模型的使用

from pydantic import BaseModel

class User(BaseModel):
    id: int
    name = 'Jane Doe'

在这里,User 是具有两个字段的模型,其中字段 id 是整数类型,并且是必需的;name 字段是字符串类型,但不是必需的(它有默认值)。name 的类型是从其默认值推断来的,因此,类型注解不是必需的(但是,当某些字段没有类型注解时,请注意[这个](# 3.1.11 字段排序)关于字段顺序的警告)。

user = User(id='123')

这里的 user 是 User 的一个实例。对象的初始化会执行所有解析和验证,如果没有引发 ValidationError 异常,则表明结果模型实例是有效的。

assert user.id == 123

模型的字段可以作为User 对象的普通属性访问。字符串 ‘123’ 根据字段类型被强制转换为 int 类型:

assert user.name == 'Jane Doe'

初始化 User 对象时未设置 name 的值,所以它的值是默认值:

assert user.__fields_set__ == {'id'}

User 对象初始化时提供的字段:

assert user.dict() == dict(user) == {'id': 123, 'name': 'Jane Doe'}

.dict() 或 dict(user) 都将会提供一个字段的字典,但 .dict() 可以接受许多其他参数:

user.id = 321
assert user.id == 321

这个模型是可变模型,所以字段值可以更改。

3.1.1.1 模型属性

上面的例子只展示了模型所能做的事情的冰山一角。模型具有以下方法和属性:

  • dict()返回模型的字段和值的字典。参见 [导出模型](# 3.6.1 model.dict(…))。
  • json()返回表示 dict() 的 JSON 字符串。参见 [导出模型](# 3.6.1 model.dict(…))。
  • copy()返回模型的副本(默认情况下为浅副本)。参见 [导出模型](# 3.6.1 model.dict(…))。
  • parse_obj()如果一个对象不是字典,可以使用该方法将其加载到具有错误处理的模型中。参见 [帮助函数](# 3.1.5 帮助函数)。
  • parse_raw()用于加载多种格式字符串的实用程序。参见 [帮助函数](# 3.1.5 帮助函数)。
  • parse_file()与 parse_raw() 类似,但是作用于文件路径。参见 [帮助函数](# 3.1.5 帮助函数)。
  • from_orm()从任意类加载数据到模型中。参见 [ORM 模式](# 3.1.3 ORM 模式)。
  • schema()返回一个将模型表示为 JSON 模式的字典。参见 [模式](# 3.5 模式)。
  • schema_json()返回表示 schema() 的 JSON 字符串。参见 [模式](# 3.5 模式)。
  • construct()用于创建模型而不执行验证的类方法;参见 [创建未经验证的模型](# 3.1.5.1 创建未经验证的模型)。
  • __fields_set__当模型实例初始化时设置的字段名称集合。
  • __config__模型的配置类。参见 [模型配置](# 3.4 模型配置)。

3.1.2 递归模型

可以通过在注解中使用模型本身作为类型来定义更复杂的分层数据结构。

from typing import List
from pydantic import BaseModel


class Foo(BaseModel):
    count: int
    size: float = None


class Bar(BaseModel):
    apple = 'x'
    banana = 'y'


class Spam(BaseModel):
    foo: Foo
    bars: List[Bar]


m = Spam(foo={'count': 4}, bars=[{'apple': 'x1'}, {'apple': 'x2'}])
print(m)
#> foo=Foo(count=4, size=None) bars=[Bar(apple='x1', banana='y'),
#> Bar(apple='x2', banana='y')]
print(m.dict())
"""
{
    'foo': {'count': 4, 'size': None},
    'bars': [
        {'apple': 'x1', 'banana': 'y'},
        {'apple': 'x2', 'banana': 'y'},
    ],
}
"""

对于自引用模型,参见 [延迟注解](# 3.10 延迟注解)。

3.1.3 ORM 模式

可以从任意类实例创建Pydantic模型,以支持映射到ORM对象的模型。

要达到这个目的,需要:

  • 模型的内部类 [Config](# 3.4 模型配置) 的 orm_mode 属性必须设置为 True。
  • 必须使用特殊的构造函数 from_orm 来创建模型实例。

这里的示例使用SQLAlchemy,但是相同的方法应该适用于任何ORM。

from typing import List
from sqlalchemy import Column, Integer, String
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.ext.declarative import declarative_base
from pydantic import BaseModel, constr

Base = declarative_base()


class CompanyOrm(Base):
    __tablename__ = 'companies'
    id = Column(Integer, primary_key=True, nullable=False)
    public_key = Column(String(20), index=True, nullable=False, unique=True)
    name = Column(String(63), unique=True)
    domains = Column(ARRAY(String(255)))


class CompanyModel(BaseModel):
    id: int
    public_key: constr(max_length=20)
    name: constr(max_length=63)
    domains: List[constr(max_length=255)]

    class Config:
        orm_mode = True


co_orm = CompanyOrm(
    id=123,
    public_key='foobar',
    name='Testing',
    domains=['example.com', 'foobar.com'],
)
print(co_orm)
#> <models_orm_mode.CompanyOrm object at 0x7f2e727a27c0>
co_model = CompanyModel.from_orm(co_orm)
print(co_model)
#> id=123 public_key='foobar' name='Testing' domains=['example.com',
#> 'foobar.com']
3.1.3.1 保留名称

您可能想使用保留的SQLAlchemy字段命名列。在这种情况下,可以使用字段别名:

import typing

from pydantic import BaseModel, Field
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base


class MyModel(BaseModel):
    metadata: typing.Dict[str, str] = Field(alias='metadata_')

    class Config:
        orm_mode = True


BaseModel = declarative_base()


class SQLModel(BaseModel):
    __tablename__ = 'my_table'
    id = sa.Column('id', sa.Integer, primary_key=True)
    # 'metadata' is reserved by SQLAlchemy, hence the '_'
    metadata_ = sa.Column('metadata', sa.JSON)


sql_model = SQLModel(metadata_={'key': 'val'}, id=1)

pydantic_model = MyModel.from_orm(sql_model)

print(pydantic_model.dict())
#> {'metadata': {'key': 'val'}}
print(pydantic_model.dict(by_alias=True))
#> {'metadata_': {'key': 'val'}}

注意

上面的示例之所以能够工作,是因为对于字段填充,别名优先于字段名。访问 SQLModel 的 metadata 属性将导致ValidationError。

3.1.3.2 递归 ORM 模型

ORM实例将使用 from_orm 递归解析,也可以在顶层解析。

这里使用一个普通类来演示这个原理,但是可以使用任何ORM类。

from typing import List
from pydantic import BaseModel


class PetCls:
    def __init__(self, *, name: str, species: str):
        self.name = name
        self.species = species


class PersonCls:
    def __init__(self, *, name: str, age: float = None, pets: List[PetCls]):
        self.name = name
        self.age = age
        self.pets = pets


class Pet(BaseModel):
    name: str
    species: str

    class Config:
        orm_mode = True


class Person(BaseModel):
    name: str
    age: float = None
    pets: List[Pet]

    class Config:
        orm_mode = True


bones = PetCls(name='Bones', species='dog')
orion = PetCls(name='Orion', species='cat')
anna = PersonCls(name='Anna', age=20, pets=[bones, orion])
anna_model = Person.from_orm(anna)
print(anna_model)
#> name='Anna' age=20.0 pets=[Pet(name='Bones', species='dog'),
#> Pet(name='Orion', species='cat')]

Pydantic使用 GetterDict 类 (请参阅 util.py) 处理任意类,该类试图为任何类提供一个类似于字典的接口。可以通过将 GetterDict 的自定义子类设置为 Config.getter_dict 的值来覆盖默认行为(参考 [模型配置](# 3.4 模型配置))。

您还可以使用带有 pre=True 的 root_validators 定制类验证。在这种情况下,validator 函数将被传递一个可以复制和修改的GetterDict 实例。

3.1.4 错误处理

每当在正在验证的数据中发现错误时,pydantic都会引发 ValidationError。

注意

验证代码不应该引发 ValidationError 本身,而是引发 ValueError、TypeError 或 AssertionError(或 ValueError 或TypeError 的子类),这些异常将被捕获并用于填充 ValidationError。

无论发现的错误数量如何,都只会出现一个异常,ValidationError 将包含关于所有错误及其发生方式的信息。

你可以通过几种方式访问这些错误:

  • e.errors()返回输入数据中发现的错误的列表。
  • e.json()返回一个表示 errors 的 JSON。
  • str(e)返回人类可读的错误表示。

每一个错误对象包含:

  • loc错误的位置列表。列表中的第一项将是发生错误的字段,如果该字段是[子模型](# 3.1.2 递归模型),则将出现后续项以指示错误的嵌套位置。
  • type计算机可读的错误类型的标识符。
  • msg人类可读的错误解释。
  • ctx包含呈现错误消息所需的值的可选对象。

下面是一个示例:

from typing import List
from pydantic import BaseModel, ValidationError, conint


class Location(BaseModel):
    lat = 0.1
    lng = 10.1


class Model(BaseModel):
    is_required: float
    gt_int: conint(gt=42)
    list_of_ints: List[int] = None
    a_float: float = None
    recursive_model: Location = None


data = dict(
    list_of_ints=['1', 2, 'bad'],
    a_float='not a float',
    recursive_model={'lat': 4.2, 'lng': 'New York'},
    gt_int=21,
)

try:
    Model(**data)
except ValidationError as e:
    print(e)
    """
    5 validation errors for Model
    is_required
      field required (type=value_error.missing)
    gt_int
      ensure this value is greater than 42 (type=value_error.number.not_gt;
    limit_value=42)
    list_of_ints -> 2
      value is not a valid integer (type=type_error.integer)
    a_float
      value is not a valid float (type=type_error.float)
    recursive_model -> lng
      value is not a valid float (type=type_error.float)
    """

try:
    Model(**data)
except ValidationError as e:
    print(e.json())
    """
    [
      {
        "loc": [
          "is_required"
        ],
        "msg": "field required",
        "type": "value_error.missing"
      },
      {
        "loc": [
          "gt_int"
        ],
        "msg": "ensure this value is greater than 42",
        "type": "value_error.number.not_gt",
        "ctx": {
          "limit_value": 42
        }
      },
      {
        "loc": [
          "list_of_ints",
          2
        ],
        "msg": "value is not a valid integer",
        "type": "type_error.integer"
      },
      {
        "loc": [
          "a_float"
        ],
        "msg": "value is not a valid float",
        "type": "type_error.float"
      },
      {
        "loc": [
          "recursive_model",
          "lng"
        ],
        "msg": "value is not a valid float",
        "type": "type_error.float"
      }
    ]
    """

json()方法默认设置了 indent=2,但是我在这里和下面对JSON 进行了调整,使其更加简洁。

3.1.4.1 定制错误

在您的自定义数据类型或验证器中,您应该使用 ValueError、TypeError 或 AssertionError 来引发错误。

有关 @validator 装饰器的使用细节,请参阅 [验证器](# 3.3 验证器)。

from pydantic import BaseModel, ValidationError, validator


class Model(BaseModel):
    foo: str

    @validator('foo')
    def name_must_contain_space(cls, v):
        if v != 'bar':
            raise ValueError('value must be "bar"')

        return v


try:
    Model(foo='ber')
except ValidationError as e:
    print(e.errors())
    """
    [
        {
            'loc': ('foo',),
            'msg': 'value must be "bar"',
            'type': 'value_error',
        },
    ]
    """

您也可以定义自己的错误类,它可以指定自定义错误代码、消息模板和上下文:

from pydantic import BaseModel, PydanticValueError, ValidationError, validator


class NotABarError(PydanticValueError):
    code = 'not_a_bar'
    msg_template = 'value is not "bar", got "{wrong_value}"'


class Model(BaseModel):
    foo: str

    @validator('foo')
    def name_must_contain_space(cls, v):
        if v != 'bar':
            raise NotABarError(wrong_value=v)
        return v


try:
    Model(foo='ber')
except ValidationError as e:
    print(e.json())
    """
    [
      {
        "loc": [
          "foo"
        ],
        "msg": "value is not \"bar\", got \"ber\"",
        "type": "value_error.not_a_bar",
        "ctx": {
          "wrong_value": "ber"
        }
      }
    ]
    """

3.1.5 帮助函数

Pydantic 在模型上提供了三个 classmethod 帮助函数用于解析数据:

  • parse_obj这与模型中的 __init__ 方法非常相似,除了它使用的是 dict 而不是关键字参数。如果传递的对象不是 dict,则会引发ValidationError。
  • parse_raw它接受一个 str 或 bytes 并将其解析为 json,然后将结果传递给 parse_obj。适当地设置 content_type 参数也支持解析pickle 数据。
  • parse_file它接受一个文件路径,读取文件并将内容传递给 parse_raw。如果省略了 content_type,将从文件的扩展名进行推断。
import pickle
from datetime import datetime
from pathlib import Path

from pydantic import BaseModel, ValidationError


class User(BaseModel):
    id: int
    name = 'John Doe'
    signup_ts: datetime = None


m = User.parse_obj({'id': 123, 'name': 'James'})
print(m)
#> id=123 signup_ts=None name='James'

try:
    User.parse_obj(['not', 'a', 'dict'])
except ValidationError as e:
    print(e)
    """
    1 validation error for User
    __root__
      User expected dict not list (type=type_error)
    """

# assumes json as no content type passed
m = User.parse_raw('{"id": 123, "name": "James"}')
print(m)
#> id=123 signup_ts=None name='James'

pickle_data = pickle.dumps({
    'id': 123,
    'name': 'James',
    'signup_ts': datetime(2017, 7, 14)
})
m = User.parse_raw(
    pickle_data, content_type='application/pickle', allow_pickle=True
)
print(m)
#> id=123 signup_ts=datetime.datetime(2017, 7, 14, 0, 0) name='James'

path = Path('data.json')
path.write_text('{"id": 123, "name": "James"}')
m = User.parse_file(path)
print(m)
#> id=123 signup_ts=None name='James'

警告

引用 pickle,“pickle模块对于错误或恶意构造的数据是不安全的。切勿从不可信或未经身份验证的来源获取数据。”

注意

由于它可能导致任意代码的执行,因此作为一种安全措施,您需要显式地将 allow_pickle 传递给解析函数,以便加载pickle数据。

3.1.5.1 创建未经验证的模型

Pydantic还提供了 construct() 方法,允许创建未经验证的模型。当数据已经被验证或来自可信的源,并且你想要尽可能高效地创建一个模型 (使用 construct() 方法创建模型通常比创建完整验证的模型块 30 倍)。

警告

construct() 不做任何验证,这意味着它可以创建无效的模型。您务必只对已经经过验证或您信任的数据使用 construct() 方法。

from pydantic import BaseModel


class User(BaseModel):
    id: int
    age: int
    name: str = 'John Doe'


original_user = User(id=123, age=32)

user_data = original_user.dict()
print(user_data)
#> {'id': 123, 'age': 32, 'name': 'John Doe'}
fields_set = original_user.__fields_set__
print(fields_set)
#> {'id', 'age'}

# ...
# pass user_data and fields_set to RPC or save to the database etc.
# ...

# you can then create a new instance of User without
# re-running validation which would be unnecessary at this point:
new_user = User.construct(_fields_set=fields_set, **user_data)
print(repr(new_user))
#> User(name='John Doe', id=123, age=32)
print(new_user.__fields_set__)
#> {'id', 'age'}

# construct can be dangerous, only use it with validated data!:
bad_user = User.construct(id='dog')
print(repr(bad_user))
#> User(name='John Doe', id='dog')

construct() 方法的 _fields_set 关键字参数是可选的,但是允许您更精确地知道哪些字段是初始化时设置的,哪些是具有默认值的。如果该参数被省略,那么 __fields_set__ 将只包含数据提供的键。

例如,在上面的示例中,如果未提供 _fields_set 参数,new_user.__fields_set__ 将会是 {'id', 'age', 'name'}。

3.1.6 泛型模型

Pydantic支持创建泛型模型,以便更容易重用公共模型结构。

警告

泛型模型只在Python >=3.7中得到支持,这是因为在python 3.6和python 3.7之间实现泛型的方式有许多细微的变化。

为了声明泛型模型,必须执行下面的步骤:

  • 声明一个或多个 typing.TypeVar 实例来参数化您的模型。
  • 声明一个继承自 pydantic.generics.GenericModel 和 typing.Generic 的 pydantic 模型,其中将 TypeVar 实例作为参数传递给 typing.Generic 。
  • 使用 TypeVar 实例对将要使用其他类型或pydantic模型进行替换的字段进行注解。

下面是一个使用 GenericModel 创建一个易于重用的HTTP响应有效负载包装器的例子:

from typing import Generic, TypeVar, Optional, List

from pydantic import BaseModel, validator, ValidationError
from pydantic.generics import GenericModel

DataT = TypeVar('DataT')


class Error(BaseModel):
    code: int
    message: str


class DataModel(BaseModel):
    numbers: List[int]
    people: List[str]


class Response(GenericModel, Generic[DataT]):
    data: Optional[DataT]
    error: Optional[Error]

    @validator('error', always=True)
    def check_consistency(cls, v, values):
        if v is not None and values['data'] is not None:
            raise ValueError('must not provide both data and error')
        if v is None and values.get('data') is None:
            raise ValueError('must provide data or error')
        return v


data = DataModel(numbers=[1, 2, 3], people=[])
error = Error(code=404, message='Not found')

print(Response[int](data=1))
#> data=1 error=None
print(Response[str](data='value'))
#> data='value' error=None
print(Response[str](data='value').dict())
#> {'data': 'value', 'error': None}
print(Response[DataModel](data=data).dict())
"""
{
    'data': {'numbers': [1, 2, 3], 'people': []},
    'error': None,
}
"""
print(Response[DataModel](error=error).dict())
"""
{
    'data': None,
    'error': {'code': 404, 'message': 'Not found'},
}
"""
try:
    Response[int](data='value')
except ValidationError as e:
    print(e)
    """
    2 validation errors for Response[int]
    data
      value is not a valid integer (type=type_error.integer)
    error
      must provide data or error (type=value_error)
    """

如果您在泛型模型定义中设置 Config 或使用 validator,则其将以从 BaseModel 继承时相同的方式应用于具体的子类。在泛型类上定义的任何方法也将被继承。

Pydantic的泛型也正确地与mypy集成,因此如果您不使用 GenericModel 声明类型,就可以得到mypy所提供的所有类型检查。

注意

在内部,pydantic在运行时使用 create_model 生成(缓存的)具体 BaseModel,因此使用GenericModel基本上不会带来任何开销。

为了在不替换 TypeVar 实例的情况下继承 GenericModel,类也必须继承 type.Generic:

from typing import TypeVar, Generic
from pydantic.generics import GenericModel

TypeX = TypeVar('TypeX')


class BaseClass(GenericModel, Generic[TypeX]):
    X: TypeX


class ChildClass(BaseClass[TypeX], Generic[TypeX]):
    # Inherit from Generic[TypeX]
    pass


# Replace TypeX by int
print(ChildClass[int](X=1))
#> X=1

您还可以创建 GenericModel 的泛型子类,以部分或全部替换超类中的类型参数。

from typing import TypeVar, Generic
from pydantic.generics import GenericModel

TypeX = TypeVar('TypeX')
TypeY = TypeVar('TypeY')
TypeZ = TypeVar('TypeZ')


class BaseClass(GenericModel, Generic[TypeX, TypeY]):
    x: TypeX
    y: TypeY


class ChildClass(BaseClass[int, TypeY], Generic[TypeY, TypeZ]):
    z: TypeZ


# Replace TypeY by str
print(ChildClass[str, int](x=1, y='y', z=3))
#> x=1 y='y' z=3

如果具体子类的名称很重要,您还可以重写默认行为:

from typing import Generic, TypeVar, Type, Any, Tuple

from pydantic.generics import GenericModel

DataT = TypeVar('DataT')


class Response(GenericModel, Generic[DataT]):
    data: DataT
        
    @classmethod
    def __concrete_name__(cls: Type[Any], params: Tuple[Type[Any], ...]) -> str:
        return f'{params[0].__name__.title()}Response'


int_resp = Response[int](data=1)
print(int_resp)
#> data=1
print(repr(int_resp))
#> IntResponse(data=1)
str_resp = Response[str](data='a') 
print(str_resp)
#> data='a'
print(repr(str_resp))
#> StrResponse(data='a')

在嵌套模型中使用相同类型允许您在模型的不同点强制类型关系:

from typing import Generic, TypeVar

from pydantic import ValidationError
from pydantic.generics import GenericModel

T = TypeVar('T')


class InnerT(GenericModel, Generic[T]):
    inner: T


class OuterT(GenericModel, Generic[T]):
    outer: T
    nested: InnerT[T]


nested = InnerT[int](inner=1)
print(OuterT[int](outer=1, nested=nested))
#> outer=1 nested=InnerT[T][int](inner=1)
try:
    nested = InnerT[str](inner='a')
    print(OuterT[int](outer='a', nested=nested))
except ValidationError as e:
    print(e)
    """
    2 validation errors for OuterT[int]
    outer
      value is not a valid integer (type=type_error.integer)
    nested -> inner
      value is not a valid integer (type=type_error.integer)
    """

Pydantic对待 GenericModel 的方式与它对待内置泛型类型(如 List 和 Dict )的方式类似,比如让它们保持未参数化,或者使用有界(bounded)类型实例:

  • 如果在实例化泛型模型之前没有指定参数,那么它们将被视为 Any。
  • 您可以使用一个或多个有界(bounded)参数对模型进行参数化,以添加子类检查。

另外,与 List 和 Dict 一样,使用 TypeVar 指定的任何参数都可以在以后用具体类型替换。

from typing import Generic, TypeVar

from pydantic import ValidationError
from pydantic.generics import GenericModel

AT = TypeVar('AT')
BT = TypeVar('BT')


class Model(GenericModel, Generic[AT, BT]):
    a: AT
    b: BT


print(Model(a='a', b='a'))
#> a='a' b='a'

IntT = TypeVar('IntT', bound=int)
typevar_model = Model[int, IntT]
print(typevar_model(a=1, b=1))
#> a=1 b=1
try:
    typevar_model(a='a', b='a')
except ValidationError as exc:
    print(exc)
    """
    2 validation errors for Model[int, IntT]
    a
      value is not a valid integer (type=type_error.integer)
    b
      value is not a valid integer (type=type_error.integer)
    """

concrete_model = typevar_model[int]
print(concrete_model(a=1, b=1))
#> a=1 b=1

3.1.7 动态模型创建

有些情况下,模型的形状直到运行时才知道。为此,pydantic提供了 create_model 方法来允许动态创建模型。

from pydantic import BaseModel, create_model

DynamicFoobarModel = create_model('DynamicFoobarModel', foo=(str, ...), bar=123)


class StaticFoobarModel(BaseModel):
    foo: str
    bar: int = 123

在这里,StaticFooBarModel 和 DynamicFooBarModel 是相同的。

警告

请参阅 [具有 Optional 注解的必须字段](# 3.1.12.1 具有 Optional 注解的必须字段) 中的注释,以了解使用省略号作为字段默认值的字段与仅含有注解的字段之间的区别。更多细节请参见samuelcolvin/pydantic#1047。

字段可以由 (<type>, <default value>) 形式的元组定义,也可以仅由默认值定义。__config__ 和 __base__ 这两个特殊的关键字参数可以用来定制新模型。这包括使用额外的字段拓展基本模型。

from pydantic import BaseModel, create_model


class FooModel(BaseModel):
    foo: str
    bar: int = 123


BarModel = create_model(
    'BarModel',
    apple='russet',
    banana='yellow',
    __base__=FooModel,
)
print(BarModel)
#> <class 'pydantic.main.BarModel'>
print(BarModel.__fields__.keys())
#> dict_keys(['foo', 'bar', 'apple', 'banana'])

也可以通过给 __validators__ 参数传递一个字典来添加验证器:

from pydantic import create_model, ValidationError, validator


def username_alphanumeric(cls, v):
    assert v.isalnum(), 'must be alphanumeric'
    return v


validators = {
    'username_validator':
    validator('username')(username_alphanumeric)
}

UserModel = create_model(
    'UserModel',
    username=(str, ...),
    __validators__=validators
)

user = UserModel(username='scolvin')
print(user)
#> username='scolvin'

try:
    UserModel(username='scolvi%n')
except ValidationError as e:
    print(e)
    """
    1 validation error for UserModel
    username
      must be alphanumeric (type=assertion_error)
    """

3.1.8 自定义根类型

可以通过声明 __root__ 字段来使用自定义根类型定义Pydantic 模型。

根类型可以是 pydantic 支持的任意类型,该类型通过 __root__ 字段上的类型提示来指定。根值可以通过模型的 __init__ 方法的 __root__ 参数传递,或者作为 parse_obj 的第一个且唯一的一个参数。

from typing import List
import json
from pydantic import BaseModel
from pydantic.schema import schema


class Pets(BaseModel):
    __root__: List[str]


print(Pets(__root__=['dog', 'cat']))
#> __root__=['dog', 'cat']
print(Pets(__root__=['dog', 'cat']).json())
#> ["dog", "cat"]
print(Pets.parse_obj(['dog', 'cat']))
#> __root__=['dog', 'cat']
print(Pets.schema())
"""
{
    'title': 'Pets',
    'type': 'array',
    'items': {'type': 'string'},
}
"""
pets_schema = schema([Pets])
print(json.dumps(pets_schema, indent=2))
"""
{
  "definitions": {
    "Pets": {
      "title": "Pets",
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  }
}
"""

如果使用字典作为第一个参数调用一个具有自定义根类型的模型的 parse_obj 方法,将会应用下面的逻辑:

  • 如果自定义的根类型是映射类型(例如 Dict 或 Mapping),参数本身总是根据自定义根类型进行验证。
  • 对于其他自定义根类型,如果字典仅有一个键,且其名称为 __root__,则该键对应的值将根据自定义根类型进行验证。

下面是一个示例:

from typing import List, Dict
from pydantic import BaseModel, ValidationError


class Pets(BaseModel):
    __root__: List[str]


print(Pets.parse_obj(['dog', 'cat']))
#> __root__=['dog', 'cat']
print(Pets.parse_obj({'__root__': ['dog', 'cat']}))  # not recommended
#> __root__=['dog', 'cat']


class PetsByName(BaseModel):
    __root__: Dict[str, str]


print(PetsByName.parse_obj({'Otis': 'dog', 'Milo': 'cat'}))
#> __root__={'Otis': 'dog', 'Milo': 'cat'}
try:
    PetsByName.parse_obj({'__root__': {'Otis': 'dog', 'Milo': 'cat'}})
except ValidationError as e:
    print(e)
    """
    1 validation error for PetsByName
    __root__ -> __root__
      str type expected (type=type_error.str)
    """

警告

在字典上调用 parse_obj 方法时,对于非映射的自定义根类型使用单个键 "__root__",目前仅为保持向后兼容而支持,但不建议这样做,将来的版本中可能会删除。

如果您想直接访问剩余的 __root__ 字段中的条目,或者在这些条目上迭代,您可以实现自定义的 __iter__ 和 __getitem__ 函数,如下面的例子所示。

from typing import List
from pydantic import BaseModel


class Pets(BaseModel):
    __root__: List[str]

    def __iter__(self):
        return iter(self.__root__)

    def __getitem__(self, item):
        return self.__root__[item]


pets = Pets.parse_obj(['dog', 'cat'])
print(pets[0])
#> dog
print([pet for pet in pets])
#> ['dog', 'cat']

3.1.9 伪不变性

可以通过 allow_mutation = False 将模型配置为不可变的。当设置了这个值时,尝试更改实例属性的值将会引发错误。有 Config 的更多细节,请参见[模型配置](# 3.4 模型配置)。

警告

Python中的不变性从来都不是严格的。如果开发人员是果断的/愚蠢的,他们总是可以修改一个所谓的 “不可变” 对象。

from pydantic import BaseModel


class FooBarModel(BaseModel):
    a: str
    b: dict

    class Config:
        allow_mutation = False


foobar = FooBarModel(a='hello', b={'apple': 'pear'})

try:
    foobar.a = 'different'
except TypeError as e:
    print(e)
    #> "FooBarModel" is immutable and does not support item assignment

print(foobar.a)
#> hello
print(foobar.b)
#> {'apple': 'pear'}
foobar.b['apple'] = 'grape'
print(foobar.b)
#> {'apple': 'grape'}

尝试更改 a 将会引发一个错误,而 a 则保持不变。然而,字典 b 是可变的,并且 foobar 的不可变性不能阻止 b 被改变。

3.1.10 抽象基类

Pydantic模型可以与 Python 的抽象基类一起使用。

import abc
from pydantic import BaseModel


class FooBarModel(BaseModel, abc.ABC):
    a: str
    b: int

    @abc.abstractmethod
    def my_abstract_method(self):
        pass

3.1.11 字段排序

由于以下原因,字段顺序是很重要的:

  • 验证是按照字段的定义顺序执行的;[字段验证器](# 3.3 验证器) 可以访问前一个字段的值,但不能访问后一个字段的值。
  • 字段顺序保留在模型 [模式](# 3.5 模式) 中。
  • 字段顺序保留在 [验证错误](# 3.1.4 错误处理) 中。
  • 字段顺序被 .dict() 和 .json() 等保留。

从v1.0开始,所有具有注解的字段(无论是只有注解还是具有默认值)都将位于没有注解的字段之前。在它们各自的组中,字段按照定义时的顺序保存。

from pydantic import BaseModel, ValidationError


class Model(BaseModel):
    a: int
    b = 2
    c: int = 1
    d = 0
    e: float


print(Model.__fields__.keys())
#> dict_keys(['a', 'c', 'e', 'b', 'd'])
m = Model(e=2, a=1)
print(m.dict())
#> {'a': 1, 'c': 1, 'e': 2.0, 'b': 2, 'd': 0}
try:
    Model(a='x', b='x', c='x', d='x', e='x')
except ValidationError as e:
    error_locations = [e['loc'] for e in e.errors()]

print(error_locations)
#> [('a',), ('c',), ('e',), ('b',), ('d',)]

警告

如上例所示,在同一个模型中结合使用带注释的字段和不带注释的字段会导致惊人的字段顺序。(这是由于Python的限制)

因此,我们建议向所有字段添加类型注解,即使默认值将自行确定类型以确保保留字段顺序。

3.1.12 必需字段

要将字段声明为必需的,可以只使用注释来声明它,或者可以使用省略号(...)作为值:

from pydantic import BaseModel, Field


class Model(BaseModel):
    a: int
    b: int = ...
    c: int = Field(...)

其中 Field 指的是 [字段函数](# 3.5.1 字段定制)。

在这里,a、b 和 c 都是必需的。然而,在 b 中使用省略号将会导致在 mypy 中不能很好的工作,在v1.0中大多数情况下都应该避免使用。

3.1.12.1 具有 Optional 注解的必须字段

警告

从版本v1.2开始,仅含有注解的可空(Optional[...]、Union[None, ...] 和 Any)字段与以 ... 作为默认值的可空字段不再具有相同的含义。

在某些情况下,这可能导致v1.2不能完全向后兼容早期的**v1.***版本。

如果想要指定一个可接受 None 值的必须字段,可以使用具有 ... 的 Optional:

from typing import Optional
from pydantic import BaseModel, Field, ValidationError


class Model(BaseModel):
    a: Optional[int]
    b: Optional[int] = ...
    c: Optional[int] = Field(...)


print(Model(b=1, c=2))
#> a=None b=1 c=2
try:
    Model(a=1, b=2)
except ValidationError as e:
    print(e)
    """
    1 validation error for Model
    c
      field required (type=value_error.missing)
    """

在这个模型中,a、b 和 c 都可以接受 None 作为值。但 a 是可选的,而 b 和 c 都是必需的。b 和 c 需要一个值,即使这个值是 None。

3.1.13 具有动态默认值的字段

当声明一个带有默认值的字段时,你也许想让这个默认值是动态的(例如,对于每一个模型来说是不同的),则可以通过 default_factory 来实现。

Beta 版本

default_factory 参数仍处于beta版本,它是在v1.5中临时添加到pydantic的。它可能会在未来的版本中发生重大的变化,它的签名或者行为直到v2才会稳定。在它还处于临时阶段时,来自社区的反馈将非常有用;评论#866或者创建一个新问题。

示例用法:

from datetime import datetime
from uuid import UUID, uuid4
from pydantic import BaseModel, Field


class Model(BaseModel):
    uid: UUID = Field(default_factory=uuid4)
    updated: datetime = Field(default_factory=datetime.utcnow)


m1 = Model()
m2 = Model()
print(f'{m1.uid} != {m2.uid}')
#> 27ce808f-9293-47ac-860e-aebe5d6ffac7 != a583211f-b357-4b92-9632-cd1b3bbd2e1b
print(f'{m1.updated} != {m2.updated}')
#> 2020-10-28 20:03:32.840916 != 2020-10-28 20:03:32.840934

其中 Field 引用 [字段函数](# 3.5.1 字段定制)。

警告

default_factory 希望设置字段类型,此外,如果您希望使用 validate_all 验证默认值,则pydantic需要调用default_factory,这可能会导致副作用!

3.1.14 私有模型属性

如果您需要使用从模型字段中排除的内部属性,则可以使用 PrivateAttr 来声明:

from datetime import datetime
from random import randint

from pydantic import BaseModel, PrivateAttr


class TimeAwareModel(BaseModel):
    _processed_at: datetime = PrivateAttr(default_factory=datetime.now)
    _secret_value: str = PrivateAttr()

    def __init__(self, **data):
        super().__init__(**data)
        # this could also be done with default_factory
        self._secret_value = randint(1, 5)


m = TimeAwareModel()
print(m._processed_at)
#> 2020-10-28 20:03:33.179004
print(m._secret_value)
#> 5

私有属性的名称必须以下划线开始,以避免与模型字段的冲突:_attr 和 __attr__ 这两种形式都支持。

如果 Config.underscore_attrs_are_private 是 True,任何非 ClassVar 下划线属性都将被当做私有属性:

from typing import ClassVar

from pydantic import BaseModel


class Model(BaseModel):
    _class_var: ClassVar[str] = 'class var value'
    _private_attr: str = 'private attr value'

    class Config:
        underscore_attrs_are_private = True


print(Model._class_var)
#> class var value
print(Model._private_attr)
#> <member '_private_attr' of 'Model' objects>
print(Model()._private_attr)
#> private attr value

在创建类时,pydantic构造由私有属性填充的 __slots__。

3.1.15 将数据解析到特定的类型

Pydantic包含一个独立的实用程序函数 parse_obj_as,可以使用它以更特别的方式应用解析逻辑来填充Pydantic模型。这个函数的行为类似于 BaseModel.parse_obj,但是可以使用任意的pydantic兼容类型。

当您希望将结果解析为BaseModel 的直接子类之外的类型时,该函数将特别有用。例如:

from typing import List

from pydantic import BaseModel, parse_obj_as


class Item(BaseModel):
    id: int
    name: str


# `item_data` could come from an API call, eg., via something like:
# item_data = requests.get('https://my-api.com/items').json()
item_data = [{'id': 1, 'name': 'My Item'}]

items = parse_obj_as(List[Item], item_data)
print(items)
#> [Item(id=1, name='My Item')]

这个函数能够将数据解析为作为 BaseModel 字段的pydantic可以处理的任何类型。

Pydantic还包括两个类似的独立函数 parse_file_as 和 parse_raw_as,它们类似于 BaseModel.parse_file 和 BaseModel.parse_raw。

3.1.16 数据转换

pydantic可以对输入数据进行强制转换,以使其符合模型字段类型,在某些情况下,这可能会导致信息丢失。例如:

from pydantic import BaseModel


class Model(BaseModel):
    a: int
    b: float
    c: str


print(Model(a=3.1415, b=' 2.72 ', c=123).dict())
#> {'a': 3, 'b': 2.72, 'c': '123'}

这是经过深思熟虑的决定,通常是最有用的方法。在这里可以看到关于这个问题的更长的讨论。

3.1.17 模型签名

所有pydantic模型将根据其字段生成签名:

import inspect
from pydantic import BaseModel, Field


class FooModel(BaseModel):
    id: int
    name: str = None
    description: str = 'Foo'
    apple: int = Field(..., alias='pear')


print(inspect.signature(FooModel))
#> (*, id: int, name: str = None, description: str = 'Foo', pear: int) -> None

准确的签名对于自省目的和像 FastAPI 或 hypothesis 这样的库非常有用。

生成的签名也将尊重定制的 __init__ 函数:

import inspect

from pydantic import BaseModel


class MyModel(BaseModel):
    id: int
    info: str = 'Foo'

    def __init__(self, id: int = 1, *, bar: str, **data) -> None:
        """My custom init!"""
        super().__init__(id=id, bar=bar, **data)


print(inspect.signature(MyModel))
#> (id: int = 1, *, bar: str, info: str = 'Foo') -> None

要将一个字段包含在签名中,其别名或名称必须是有效的Python标识符。pydantic倾向于使用别名而不是名称,但如果别名不是有效的python标识符,则可以使用字段名称。

如果字段的别名和名称都是无效标识符,则将添加一个 **data 参数。此外,如果 Config.extra 是 Extra.allow,则 **data 参数将始终出现在签名中。

注意

模型签名中的类型与模型注释中声明的类型相同,不一定是可以实际提供给该字段的所有类型。这个问题可能会在#1055被解决后被修复。

3.2 字段类型

pydantic在可能的情况下使用[标准库类型](# 3.2.1 标准库类型)定义字段,从而使学习曲线更加平滑。 但是,对于许多有用的应用程序,不存在标准库类型,因此pydantic实现了[许多常用类型](#3.2.3 Pydantic 类型)。

如果现有类型无法满足需求,您还可以使用自定义属性和验证来实现[与pydantic兼容的自定义数据类型](# 3.2.7 自定义数据类型)。

3.2.1 标准库类型

pydantic支持python标准库中的许多常见类型。如果需要更严格的处理,请查看[严格类型](# 3.2.5 Strict类型);如果您需要对允许的值进行约束(例如,要求一个正整数),请参阅[约束类型](# 3.2.4 约束类型)。

  • bool有关如何验证布尔值以及允许哪些值的详细信息,请参阅下面的[布尔类型](# 3.2.1.6 Boolean)。
  • intpydantic 使用 int(v) 来强制将一个值转换为 int 类型。请参阅[这里](# 3.1.16 数据转换)关于数据转换期间信息丢失的警告。
  • float同样的,使用 float(v) 来将一个值强制转换为 float 类型。
  • str字符串按原样接受,int、float 和 Decimal 使用 str(v) 强制转换,bytes 和 bytearray 使用 v.decode() 转换,继承自 str 的枚举使用 v.value 转换,所有其他类型都会导致错误。
  • bytes字节是按原格式接受的,bytearray 使用 bytes(v) 进行转换,str 使用 v.encode() 进行转换,int、float 和 Decimal 使用 str(v).encode() 进行转换。
  • list允许 list、tuple、set、frozenset、deque 或生成器,并会转换成列表。对于子类型约束,参见下面的 typing.List 。
  • tuple允许 list、tuple、set、frozenset、deque 或生成器,并会转换成元组。对于子类型约束,参见下面的 typing.Tuple 。
  • dict会尝试使用 dict(v) 来转换为字典。对于子类型约束,参见下面的 typing.Dict 。
  • set允许 list、tuple、set、frozenset、deque 或生成器,并会转换成集合。对于子类型约束,参见下面的 typing.Set 。
  • frozenset允许 list、tuple、set、frozenset、deque 或生成器,并会转换成冻结集合。对于子类型约束,参见下面的 typing.FrozenSet 。
  • deque允许 list、tuple、set、frozenset、deque 或生成器,并会转换成双端队列。对于子类型约束,参见下面的 typing.Deque 。
  • datetime.date有关解析和验证的更多细节,请参见下面的[日期时间类型](# 3.2.1.5 Datetime)。
  • datetime.time有关解析和验证的更多细节,请参见下面的[日期时间类型](# 3.2.1.5 Datetime)。
  • datetime.datetime有关解析和验证的更多细节,请参见下面的[日期时间类型](# 3.2.1.5 Datetime)。
  • datetime.timedelta有关解析和验证的更多细节,请参见下面的[日期时间类型](# 3.2.2.3 Datetime)。
  • typing.Any允许任何值,包括 None,因此一个 Any 字段是可选的。
  • typing.TypeVar根据 constraints 或 bound 约束允许的值。参见 [TypeVar](# 3.2.1.9 TypeVar)。
  • typing.Union有关解析和验证的更多细节,请参见[Union](# 3.2.1.3 Union)。
  • typing.OptionalOptional[x] 是 Union[x, None] 的快捷方式。有关解析和验证的详细信息,请参阅[Union](# 3.2.1.3 Union);有关可以接收 None 作为值的必需字段的详细信息,请参阅 [必需字段](# 3.1.12 必需字段)。
  • typing.List有关解析和验证的更多细节,参见 [typing中的可迭代类型](# 3.2.1.1 typing 中的可迭代类型)。
  • typing.Tuple有关解析和验证的更多细节,参见 [typing中的可迭代类型。](# 3.2.1.1 typing 中的可迭代类型)
  • typing.Dict有关解析和验证的更多细节,参见 [typing中的可迭代类型。](# 3.2.1.1 typing 中的可迭代类型)
  • typing.Set有关解析和验证的更多细节,参见 [typing中的可迭代类型。](# 3.2.1.1 typing 中的可迭代类型)
  • typing.FrozenSet有关解析和验证的更多细节,参见 [typing中的可迭代类型。](# 3.2.1.1 typing 中的可迭代类型)
  • typing.Deque有关解析和验证的更多细节,参见 [typing中的可迭代类型。](# 3.2.1.1 typing 中的可迭代类型)
  • typing.Sequence有关解析和验证的更多细节,参见 [typing中的可迭代类型。](# 3.2.1.1 typing 中的可迭代类型)
  • typing.Iterable这是为不应该被消耗的可迭代对象保留的。有关解析和验证的更多细节,请参阅下面的[无限生成器](# 3.2.1.2 无限生成器)。
  • typing.Type有关解析和验证的更多细节,参见 [Type](# 3.2.1.8 Type)。
  • typing.Callable有关解析和验证的更多细节,参见 [Callable](# 3.2.1.7 Callable)。
  • typing.Pattern将会导致输入值被传递到 re.compile(v) 以创建一个正则表达式模式。
  • ipaddress.IPv4Address通过将值传递给 IPv4Address(v),简单地使用类型本身进行验证;有关其他自定义IP地址类型,请参阅[Pydantic类型](# 3.2.3 Pydantic 类型)。
  • ipaddress.IPv4Interface通过将值传递给 IPv4Interface(v),简单地使用类型本身进行验证;有关其他自定义IP地址类型,请参阅[Pydantic类型](3.2.3 Pydantic 类型)。
  • ipaddress.IPv4Network通过将值传递给 IPv4Network(v),简单地使用类型本身进行验证;有关其他自定义IP地址类型,请参阅[Pydantic类型](3.2.3 Pydantic 类型)。
  • ipaddress.IPv6Address通过将值传递给 IPv6Address(v),简单地使用类型本身进行验证;有关其他自定义IP地址类型,请参阅[Pydantic类型](3.2.3 Pydantic 类型)。
  • ipaddress.IPv6Interface通过将值传递给 IPv6Interface(v),简单地使用类型本身进行验证;有关其他自定义IP地址类型,请参阅[Pydantic类型](3.2.3 Pydantic 类型)。
  • ipaddress.IPv6.Network通过将值传递给 IPv6Network(v),简单地使用类型本身进行验证;有关其他自定义IP地址类型,请参阅[Pydantic类型](3.2.3 Pydantic 类型)。
  • enum.Enum检查值是有效的 Enum 实例。
  • subclass of enum.Enum检查值是有效的枚举成员。更多细节,参见 [Enum 和 Choice](# 3.2.1.4 Enum 和 Choice)。
  • enum.IntEnum检查值是有效的 IntEnum 实例。
  • subclass of enum.IntEnum检查值是有效的整数枚举成员。更多细节,参见 [Enum 和 Choice](# 3.2.1.4 Enum 和 Choice)。
  • decimal.Decimalpydantic 会尝试将值转换为字符串,然后将那个字符串传递个 Decimal(v)。
  • pathlib.Path通过将值传递给 Path(v) ,简单地使用类型本身进行验证。对于其他更严格的路径类型,参见 [Pydantic类型](3.2.3 Pydantic 类型)。
  • uuid.UUID字符串和字节(转换为字符串)被传递到 UUID(v),对于 bytes 和 bytearray ,回退到 UUID(bytes=v);有关其他更严格的UUID类型,请参见 [Pydantic类型](3.2.3 Pydantic 类型)。
  • ByteSize将带有单位的字节字符串转换为字节。
3.2.1.1 typing 中的可迭代类型

Pydantic使用在PEP 484中定义的标准库 typing 来定义复杂对象。

from typing import (
    Deque, Dict, FrozenSet, List, Optional, Sequence, Set, Tuple, Union
)

from pydantic import BaseModel


class Model(BaseModel):
    simple_list: list = None
    list_of_ints: List[int] = None

    simple_tuple: tuple = None
    tuple_of_different_types: Tuple[int, float, str, bool] = None

    simple_dict: dict = None
    dict_str_float: Dict[str, float] = None

    simple_set: set = None
    set_bytes: Set[bytes] = None
    frozen_set: FrozenSet[int] = None

    str_or_bytes: Union[str, bytes] = None
    none_or_str: Optional[str] = None

    sequence_of_ints: Sequence[int] = None

    compound: Dict[Union[str, bytes], List[Set[int]]] = None

    deque: Deque[int] = None


print(Model(simple_list=['1', '2', '3']).simple_list)
#> ['1', '2', '3']
print(Model(list_of_ints=['1', '2', '3']).list_of_ints)
#> [1, 2, 3]

print(Model(simple_dict={'a': 1, b'b': 2}).simple_dict)
#> {'a': 1, b'b': 2}
print(Model(dict_str_float={'a': 1, b'b': 2}).dict_str_float)
#> {'a': 1.0, 'b': 2.0}

print(Model(simple_tuple=[1, 2, 3, 4]).simple_tuple)
#> (1, 2, 3, 4)
print(Model(tuple_of_different_types=[4, 3, 2, 1]).tuple_of_different_types)
#> (4, 3.0, '2', True)

print(Model(sequence_of_ints=[1, 2, 3, 4]).sequence_of_ints)
#> [1, 2, 3, 4]
print(Model(sequence_of_ints=(1, 2, 3, 4)).sequence_of_ints)
#> (1, 2, 3, 4)

print(Model(deque=[1, 2, 3]).deque)
#> deque([1, 2, 3])
3.2.1.2 无限生成器

如果你有一个生成器,您可以使用上面所述的 Sequence。在这种情况下,生成器将被消耗并作为列表存储在模型中,其中的值将使用Sequence 的子类型进行验证(例如 Sequence[int] 中的 int)。

但是如果您有一个生成器,并且不想其被消耗,例如无限生成器或远程数据加载器,则可以用 Iterable 定义其类型:

from typing import Iterable
from pydantic import BaseModel


class Model(BaseModel):
    infinite: Iterable[int]


def infinite_ints():
    i = 0
    while True:
        yield i
        i += 1


m = Model(infinite=infinite_ints())
print(m)
#> infinite=<generator object infinite_ints at 0x7fcb8ff44580>

for i in m.infinite:
    print(i)
    #> 0
    #> 1
    #> 2
    #> 3
    #> 4
    #> 5
    #> 6
    #> 7
    #> 8
    #> 9
    #> 10
    if i == 10:
        break

警告

对于类型注解为 Iterable 的字段,只执行简单的检查,以确定其是可迭代的,并且该可迭代对象不会被消费。

不执行对它们的值的验证,因为如果不消费可迭代对象,就无法进行验证。

提示

如果您想要验证无限生成器的值,您可以创建一个单独的模型,并在使用生成器时使用它,并根据需要报告验证错误。

pydantic无法为您自动验证这些值,因为如果要进行验证,则必须消费无限生成器。

3.2.1.2.1 验证无限生成器的第一个值

您可以创建一个验证器来验证无限生成器中的第一个值,但仍然不完全使用它。

import itertools
from typing import Iterable
from pydantic import BaseModel, validator, ValidationError
from pydantic.fields import ModelField


class Model(BaseModel):
    infinite: Iterable[int]

    @validator('infinite')
    # You don't need to add the "ModelField", but it will help your
    # editor give you completion and catch errors
    def infinite_first_int(cls, iterable, field: ModelField):
        first_value = next(iterable)
        if field.sub_fields:
            # The Iterable had a parameter type, in this case it's int
            # We use it to validate the first value
            sub_field = field.sub_fields[0]
            v, error = sub_field.validate(first_value, {}, loc='first_value')
            if error:
                raise ValidationError([error], cls)
        # This creates a new generator that returns the first value and then
        # the rest of the values from the (already started) iterable
        return itertools.chain([first_value], iterable)


def infinite_ints():
    i = 0
    while True:
        yield i
        i += 1


m = Model(infinite=infinite_ints())
print(m)
#> infinite=<itertools.chain object at 0x7fcb8ff18c10>


def infinite_strs():
    while True:
        for letter in 'allthesingleladies':
            yield letter


try:
    Model(infinite=infinite_strs())
except ValidationError as e:
    print(e)
    """
    1 validation error for Model
    infinite -> first_value
      value is not a valid integer (type=type_error.integer)
    """
3.2.1.3 Union

Union 类型允许一个模型属性接受不同的类型,例如:

警告

此脚本是完整的,应该 “按原样” 运行。然而,它可能不能反映所期望的行为;见下文。

from uuid import UUID
from typing import Union
from pydantic import BaseModel


class User(BaseModel):
    id: Union[int, str, UUID]
    name: str


user_01 = User(id=123, name='John Doe')
print(user_01)
#> id=123 name='John Doe'
print(user_01.id)
#> 123
user_02 = User(id='1234', name='John Doe')
print(user_02)
#> id=1234 name='John Doe'
print(user_02.id)
#> 1234
user_03_uuid = UUID('cf57432e-809e-4353-adbd-9d5c0d733868')
user_03 = User(id=user_03_uuid, name='John Doe')
print(user_03)
#> id=275603287559914445491632874575877060712 name='John Doe'
print(user_03.id)
#> 275603287559914445491632874575877060712
print(user_03_uuid.int)
#> 275603287559914445491632874575877060712

但是,如上面所示,pydantic将尝试 “匹配” Union 下定义的任何类型,并将使用第一个匹配的类型。在上面的示例中,user_03 的 id 被定义为 uuid.UUID 类 (在属性的 Union 注解下定义),但由于 uuid.UUID 可以编组为一个 int,因此它选择与 int 类型匹配,而不考虑其他类型。

因此,建议在定义 Union 注解时,首先包含最特定的类型,然后再包含不那么特定的类型。在上面的例子中,UUID 类应该在 int 和str 类之前,以排除意外的表示:

from uuid import UUID
from typing import Union
from pydantic import BaseModel


class User(BaseModel):
    id: Union[UUID, int, str]
    name: str


user_03_uuid = UUID('cf57432e-809e-4353-adbd-9d5c0d733868')
user_03 = User(id=user_03_uuid, name='John Doe')
print(user_03)
#> id=UUID('cf57432e-809e-4353-adbd-9d5c0d733868') name='John Doe'
print(user_03.id)
#> cf57432e-809e-4353-adbd-9d5c0d733868
print(user_03_uuid.int)
#> 275603287559914445491632874575877060712

提示

类型 Optional[x] 是 Union[x, None] 的快捷方式。

Optional[x] 也可以用于指定一个必需字段也可以接受 None 作为其值。

更多细节,参见 [必需字段](# 3.1.12 必需字段)。

3.2.1.4 Enum 和 Choice

Pydantic使用 Python 的标准 enum 类来定义选择:

from enum import Enum, IntEnum

from pydantic import BaseModel, ValidationError


class FruitEnum(str, Enum):
    pear = 'pear'
    banana = 'banana'


class ToolEnum(IntEnum):
    spanner = 1
    wrench = 2


class CookingModel(BaseModel):
    fruit: FruitEnum = FruitEnum.pear
    tool: ToolEnum = ToolEnum.spanner


print(CookingModel())
#> fruit=<FruitEnum.pear: 'pear'> tool=<ToolEnum.spanner: 1>
print(CookingModel(tool=2, fruit='banana'))
#> fruit=<FruitEnum.banana: 'banana'> tool=<ToolEnum.wrench: 2>
try:
    CookingModel(fruit='other')
except ValidationError as e:
    print(e)
    """
    1 validation error for CookingModel
    fruit
      value is not a valid enumeration member; permitted: 'pear', 'banana'
    (type=type_error.enum; enum_values=[<FruitEnum.pear: 'pear'>,
    <FruitEnum.banana: 'banana'>])
    """
3.2.1.5 Datetime

Pydantic 支持下面一些 datetime 了类型:

  • datetime 字段可以是:
  • datetime:存在 datetime 对象时
  • int 或 float:假定为 Unix 时间时。例如,自1970年1月1日以来的秒数 (如果 >= -2e10 或 <= 2e10) 或毫秒数 (如果 < -2e10 或 > 2e10)。
  • str:下面的格式可用时:
  • YYYY-MM-DD[T]HH:MM[:SS[.ffffff]][Z or [±]HH[:]MM]]]
  • 作为字符串的 int 或 floats (假定为Unix time)
  • date 字段可以是:
  • date:存在 date 对象时
  • int 或float: 参见 datetime
  • str:下面的格式可用时:
  • YYYY-MM-DD
  • int 或 float 参见 datetime
  • time 字段可以是:
  • time:存在 time 对象时
  • str:下面的格式可用时:
  • HH:MM[:SS[.ffffff]][Z or [±]HH[:]MM]]]
  • timedelta 字段可以是:
  • timedelta:存在 timedelta 对象时
  • int 或 float:假定为"秒"
  • str:下面的格式可用时:
  • [-][DD ][HH:MM]SS[.ffffff]
  • [±]P[DD]DT[HH]H[MM]M[SS]S (ISO 8601 的 timedelta 格式)
from datetime import date, datetime, time, timedelta
from pydantic import BaseModel


class Model(BaseModel):
    d: date = None
    dt: datetime = None
    t: time = None
    td: timedelta = None


m = Model(
    d=1966280412345.6789,
    dt='2032-04-23T10:20:30.400+02:30',
    t=time(4, 8, 16),
    td='P3DT12H30M5S',
)

print(m.dict())
"""
{
    'd': datetime.date(2032, 4, 22),
    'dt': datetime.datetime(2032, 4, 23, 10, 20, 30, 400000,
tzinfo=datetime.timezone(datetime.timedelta(seconds=9000))),
    't': datetime.time(4, 8, 16),
    'td': datetime.timedelta(days=3, seconds=45005),
}
"""
3.2.1.6 Boolean

警告

从v1.0版本开始,解析 bool 字段的逻辑发生了变化。

在v1.0之前,bool 解析从来不会失败,会导致一些意想不到的结果。新的逻辑如下所述。

如果不是下面的值之一,标准的 bool 字段将会引发 ValidationError :

  • 有效的布尔值(例如 True 或 False);
  • 整数 0 或 1;
  • 一个字符串,在转换为小写后,等于 0、off、f、false、n、no、1、on、t、true、y、yes 其中之一;
  • 一个 bytes,在解码为 str 后,依据上一个规则有效。

注意

如果想要更严格的布尔逻辑(例如,只允许 True 或 False 的字段),可以使用 StrictBool。

下面的脚本模拟了这些行为:

from pydantic import BaseModel, ValidationError


class BooleanModel(BaseModel):
    bool_value: bool


print(BooleanModel(bool_value=False))
#> bool_value=False
print(BooleanModel(bool_value='False'))
#> bool_value=False
try:
    BooleanModel(bool_value=[])
except ValidationError as e:
    print(str(e))
    """
    1 validation error for BooleanModel
    bool_value
      value could not be parsed to a boolean (type=type_error.bool)
    """
3.2.1.7 Callable

字段也可以是 Callable 类型:

from typing import Callable
from pydantic import BaseModel


class Foo(BaseModel):
    callback: Callable[[int], int]


m = Foo(callback=lambda x: x)
print(m)
#> callback=<function <lambda> at 0x7fcb90290dc0>

警告

类型为 Callble 的字段只执行简单的检查以确定参数是否可调用;不执行参数、参数类型或返回类型的验证。

3.2.1.8 Type

Pydantic支持使用 Type[T] 来指定一个字段只可以接受类 T 的子类(而不是实例)。

from typing import Type

from pydantic import BaseModel
from pydantic import ValidationError


class Foo:
    pass


class Bar(Foo):
    pass


class Other:
    pass


class SimpleModel(BaseModel):
    just_subclasses: Type[Foo]


SimpleModel(just_subclasses=Foo)
SimpleModel(just_subclasses=Bar)
try:
    SimpleModel(just_subclasses=Other)
except ValidationError as e:
    print(e)
    """
    1 validation error for SimpleModel
    just_subclasses
      subclass of Foo expected (type=type_error.subclass; expected_class=Foo)
    """

也可以使用 Type 来指定允许任意类:

from typing import Type

from pydantic import BaseModel, ValidationError


class Foo:
    pass


class LenientSimpleModel(BaseModel):
    any_class_goes: Type


LenientSimpleModel(any_class_goes=int)
LenientSimpleModel(any_class_goes=Foo)
try:
    LenientSimpleModel(any_class_goes=Foo())
except ValidationError as e:
    print(e)
    """
    1 validation error for LenientSimpleModel
    any_class_goes
      a class is expected (type=type_error.class)
    """
3.2.1.9 TypeVar

TypeVar 支持无约束(unconstrained)、有约束(constrained) 或有绑定(with bound)。

from typing import TypeVar
from pydantic import BaseModel

Foobar = TypeVar('Foobar')
BoundFloat = TypeVar('BoundFloat', bound=float)
IntStr = TypeVar('IntStr', int, str)


class Model(BaseModel):
    a: Foobar  # equivalent of ": Any"
    b: BoundFloat  # equivalent of ": float"
    c: IntStr  # equivalent of ": Union[int, str]"


print(Model(a=[1], b=4.2, c='x'))
#> a=[1] b=4.2 c='x'

# a may be None and is therefore optional
print(Model(b=1, c=1))
#> a=None b=1.0 c=1

3.2.2 Literal类型

注意:

这是自python 3.8以来的python标准库的一个新特性;在python 3.8之前,则需要安装 typing-extensions。

pydantic支持使用 typing.Literal (或者在 Python3.8之前,使用 typing_extensions.Literal) 作为一种轻量级的方式来指定一个字段只能接受特定的字面量:

from typing import Literal

from pydantic import BaseModel, ValidationError


class Pie(BaseModel):
    flavor: Literal['apple', 'pumpkin']


Pie(flavor='apple')
Pie(flavor='pumpkin')
try:
    Pie(flavor='cherry')
except ValidationError as e:
    print(str(e))
    """
    1 validation error for Pie
    flavor
      unexpected value; permitted: 'apple', 'pumpkin'
    (type=value_error.const; given=cherry; permitted=('apple', 'pumpkin'))
    """

这个字段类型的一个好处是,它可以用来检查一个或多个特定值是否相等,而不需要声明自定义验证器:

from typing import ClassVar, List, Union

from typing import Literal

from pydantic import BaseModel, ValidationError


class Cake(BaseModel):
    kind: Literal['cake']
    required_utensils: ClassVar[List[str]] = ['fork', 'knife']


class IceCream(BaseModel):
    kind: Literal['icecream']
    required_utensils: ClassVar[List[str]] = ['spoon']


class Meal(BaseModel):
    dessert: Union[Cake, IceCream]


print(type(Meal(dessert={'kind': 'cake'}).dessert).__name__)
#> Cake
print(type(Meal(dessert={'kind': 'icecream'}).dessert).__name__)
#> IceCream
try:
    Meal(dessert={'kind': 'pie'})
except ValidationError as e:
    print(str(e))
    """
    2 validation errors for Meal
    dessert -> kind
      unexpected value; permitted: 'cake' (type=value_error.const; given=pie;
    permitted=('cake',))
    dessert -> kind
      unexpected value; permitted: 'icecream' (type=value_error.const;
    given=pie; permitted=('icecream',))
    """

在带注解的 Union 中使用适当的排序,你可以使用它来解析特定的降序类型(types of descreasing):

from typing import Optional, Union

from typing import Literal

from pydantic import BaseModel


class Dessert(BaseModel):
    kind: str


class Pie(Dessert):
    kind: Literal['pie']
    flavor: Optional[str]


class ApplePie(Pie):
    flavor: Literal['apple']


class PumpkinPie(Pie):
    flavor: Literal['pumpkin']


class Meal(BaseModel):
    dessert: Union[ApplePie, PumpkinPie, Pie, Dessert]


print(type(Meal(dessert={'kind': 'pie', 'flavor': 'apple'}).dessert).__name__)
#> ApplePie
print(type(Meal(dessert={'kind': 'pie', 'flavor': 'pumpkin'}).dessert).__name__)
#> PumpkinPie
print(type(Meal(dessert={'kind': 'pie'}).dessert).__name__)
#> Pie
print(type(Meal(dessert={'kind': 'cake'}).dessert).__name__)
#> Dessert

3.2.3 Pydantic 类型

Pydantic 也提供了许多其他有用的类型:

  • FilePath与 Path 类似,但路径必须存在并且必须是文件。
  • DirectoryPath与 Path 类似,但路径必须存在并且必须是目录。
  • EmailStr需要安装 email-validator;输入字符串必须是有效的 email地址,输出是一个简单字符串。
  • NameEmail需要安装 email-validator;输入字符串必须是有效的 email地址或是 Fred Bloggs <fred.bloggs@example.com> 这样的格式,输出是一个 NameEmail 对象,该对象有两个属性:name 和 email。对于 Fred Bloggs <fred.bloggs@example.com>,名称将会是 Fred Bloggs。对于 fred.bloggs@example.com,名称将会是 fred.bloggs。
  • PyObject需要一个可调用对象或一个包含 . 的表示导入路径的字符串。如果提供的是一个字符串,则从该字符串所表示的导入路径加载最后一个 . 右边的子串所表示的可导入Python 对象。 例如,如果提供了 'math.cos',则结果字段值将为函数 cos。如果提供了 pydantic.dataclasses.dataclass,则结果字段值将为 dataclass。
  • Color用于解析 HTML 和 CSS 颜色。参见 [Color类型](3.2.3.2 Color 类型)。
  • Json一个特殊的类型包装器,在解析之前加载JSON; 参见 [JSON类型](# 3.2.3.4 Json 类型)。
  • PaymentCardNumber对支付卡进行解析和验证;参见 [PaymentCardNumber 类型](3.2.3.5 PaymentCardNumber)。
  • AnyUrl任意 URL。参见 [URL](# 3.2.3.1 URL)
  • AnyHttpUrl一个 HTTP URL。参见 [URL](# 3.2.3.1 URL)
  • HttpUrl更严格的HTTP URL。参见 [URL](# 3.2.3.1 URL)。
  • PostgresDsnPostgres DSN 样式的 URL。参见 [URL](# 3.2.3.1 URL)。
  • RedisDsnRedis DSN 样式的 URL。参见 [URL](# 3.2.3.1 URL)。
  • stricturl用于任意URL约束的类型方法。参见 [URL](# 3.2.3.1 URL)。
  • UUID1需要类型1的有效UUID。参见[上面](# 3.2.1 标准库类型)的UUID。
  • UUID3需要类型3的有效UUID。参见[上面](# 3.2.1 标准库类型)的UUID。
  • UUID4需要类型4的有效UUID。参见[上面](# 3.2.1 标准库类型)的UUID。
  • UUID5需要类型5的有效UUID。参见[上面](# 3.2.1 标准库类型)的UUID。
  • SecretBytes值部分(partially)保密的字节。参见 [Secret](# 3.2.3.3 Secret 类型)。
  • SecretStr值部分(partially)保密的字符串。参见 [Secret](# 3.2.3.3 Secret 类型)。
  • IPvAnyAddress允许一个 IPv4Address 或一个 IPv6Address。
  • IPvAnyInterface允许一个 IPv4Interface 或一个 IPv6Interface。
  • IPvAnyNetwork允许一个 IPv4Network 或一个 IPv6Network。
  • NegativeFloat允许负的浮点数; 使用标准的 float 解析,然后检查该值是否小于0; 请参阅 [约束类型](3.2.4 约束类型)。
  • NegativeInt允许负的整数; 使用标准的 int 解析,然后检查该值是否小于0; 请参阅 [约束类型](3.2.4 约束类型)。
  • PositiveFloat允许正的浮点数; 使用标准的 float 解析,然后检查该值是否大于0; 请参阅 [约束类型](3.2.4 约束类型)。
  • PosiviteInt允许正的整数; 使用标准的 int 解析,然后检查该值是否大于0; 请参阅 [约束类型](3.2.4 约束类型)。
  • conbytes用于约束 bytes 的类型方法。请参阅 [约束类型](3.2.4 约束类型)。
  • condecimal用于约束 Decimal 的类型方法。请参阅 [约束类型](3.2.4 约束类型)。
  • confloat用于约束 float 的类型方法。请参阅 [约束类型](3.2.4 约束类型)。
  • conint用于约束 int 的类型方法。请参阅 [约束类型](3.2.4 约束类型)。
  • conlist用于约束 list 的类型方法。请参阅 [约束类型](3.2.4 约束类型)。
  • conset用于约束 set 的类型方法。请参阅 [约束类型](3.2.4 约束类型)。
  • constr用于约束 str 的类型方法。请参阅 [约束类型](3.2.4 约束类型)。
3.2.3.1 URL

对于 URI/URL验证,可以使用以下类型:

  • AnyUrl允许任意方案,不需要TLD。
  • AnyHttpUrlhttp 或 https 方案,不需要TLD。
  • HttpUrlhttp 或 https 方案,需要TLD,最大长度 2083。
  • PostgresDsnpostgres 或 postgresql 方案,需要用户信息,不需要 TLD。
  • RedisDsnredis 方案,不需要用户信息。不需要 TLD (更改:从v1.6开始不需要用户信息)。
  • stricturl带有如下关键字参数的方法:
  • strip_whitespace: bool = True
  • min_length: int = 1
  • max_length: int = 2 ** 16
  • tld_required: bool = True
  • allowed_schemes: Optional[Set[str]] = None

当提供了无效的 URL 时,上面这些类型(都继承自AnyUrl)都会尝试给出描述性的错误。

from pydantic import BaseModel, HttpUrl, ValidationError


class MyModel(BaseModel):
    url: HttpUrl


m = MyModel(url='http://www.example.com')
print(m.url)
#> http://www.example.com
try:
    MyModel(url='ftp://invalid.url')
except ValidationError as e:
    print(e)
    """
    1 validation error for MyModel
    url
      URL scheme not permitted (type=value_error.url.scheme;
    allowed_schemes={'https', 'http'})
    """

try:
    MyModel(url='not a url')
except ValidationError as e:
    print(e)
    """
    1 validation error for MyModel
    url
      invalid or missing URL scheme (type=value_error.url.scheme)
    """

如果需要自定义的 URI/URL 类型,则可以按照与上面定义的类型类似的方式来创建它。

3.2.3.1.1 URL 属性

假定输入 URL 为 http://samuel:pass@example.com:8000/the/path/?query=here#fragment=is;this=bit,上面的类型导出如下的属性:

  • scheme总是设置。URL 方案( 上面的 http)
  • host总是设置。URL 主机 (上面的 example.com)
  • host_type总是设置。描述主机类型,或
  • domain例如,example.com
  • int_domain国际域名,参见[下面](# 3.2.4.1.2 国际域名),例如 exampl£e.org
  • ipv4一个 IP V4 地址,例如 127.0.0.1 或
  • ipv6一个IP V6 地址,例如 2001:db8:ff00:42
  • user可选。如果包括的话,则表示用户名 (上面的 samuel)
  • password可选。如果包括的话,则表示密码 (上面的 pass)
  • tld可选。顶级域名 (上面的 com),注意:这对于任何两级域都是错误的,例如 “co.uk”。如果您需要完整的TLD验证,则需要实施自己的TLD列表。
  • port可选。表示端口(上面的 8000)
  • path可选。表示路径(上面的 /the/path/)
  • query可选。表示 URL 查询 (也叫查询字符串或 GET 参数) (上面的 query=here)
  • fragment可选。片段 (上面的 fragment=is;this=bit)

如果需要进一步的验证,验证器可以使用这些属性来强制执行特定的行为:

from pydantic import BaseModel, HttpUrl, PostgresDsn, ValidationError, validator


class MyModel(BaseModel):
    url: HttpUrl


m = MyModel(url='http://www.example.com')

# the repr() method for a url will display all properties of the url
print(repr(m.url))
#> HttpUrl('http://www.example.com', scheme='http', host='www.example.com',
#> tld='com', host_type='domain')
print(m.url.scheme)
#> http
print(m.url.host)
#> www.example.com
print(m.url.host_type)
#> domain
print(m.url.port)
#> None


class MyDatabaseModel(BaseModel):
    db: PostgresDsn

    @validator('db')
    def check_db_name(cls, v):
        assert v.path and len(v.path) > 1, 'database must be provided'
        return v


m = MyDatabaseModel(db='postgres://user:pass@localhost:5432/foobar')
print(m.db)
#> postgres://user:pass@localhost:5432/foobar

try:
    MyDatabaseModel(db='postgres://user:pass@localhost:5432')
except ValidationError as e:
    print(e)
    """
    1 validation error for MyDatabaseModel
    db
      database must be provided (type=assertion_error)
    """
3.2.3.1.2 国际域名

“国际域名” (例如,主机或TLD包含非ASCII字符的URL) 将通过punycode进行编码(请参见本文,以了解其重要性的详细说明):

from pydantic import BaseModel, HttpUrl


class MyModel(BaseModel):
    url: HttpUrl


m1 = MyModel(url='http://puny£code.com')
print(m1.url)
#> http://xn--punycode-eja.com
print(m1.url.host_type)
#> int_domain
m2 = MyModel(url='https://www.аррӏе.com/')
print(m2.url)
#> https://www.xn--80ak6aa92e.com/
print(m2.url.host_type)
#> int_domain
m3 = MyModel(url='https://www.example.珠宝/')
print(m3.url)
#> https://www.example.xn--pbt977c/
print(m3.url.host_type)
#> int_domain
3.2.3.1.3 主机名中的下划线

警告

在Pydantic中,除TLD之外,域的所有部分都允许使用下划线。 从技术上讲,这可能是错误的——理论上,主机名不能带有下划线,而子域可以。

要解释这个,请考虑下面两种情况:

  • exam_ple.co.uk 主机名是 exam_ple,这是不被允许的,因为它包含一个下划线。
  • foo_bar.example.com 主机名是 example,这是被允许的,因为下划线在子域中。

如果没有详尽的TLD列表,就不可能在这两者之间进行区分。 因此,可以使用下划线,但是如果需要,您始终可以在验证器中进行进一步的验证。

另外,Chrome,Firefox和Safari当前都接受 http://exam_ple.com 作为URL,因此我们是一家优秀(至少是大型)公司。

3.2.3.2 Color 类型

您可以根据CSS3规范使用 Color 数据类型存储颜色。 可以通过以下方式定义颜色:

  • 名称 (例如 "Black"、"azure" )
  • 十六进制值 (例如 "0x000","#FFFFFF","7fffd4" )
  • RGB/RGBA 元组 (例如 (255, 255, 255),(255, 255, 255, 0.5) )
  • RGB/RGBA 字符串 (例如 "rgb(255, 255, 255)","rgba(255, 255, 255, 0.5)" )
  • HSL 字符串 (例如 "hsl(270, 60%, 70%)","hsl(270, 60%, 70%, .5)" )
from pydantic import BaseModel, ValidationError
from pydantic.color import Color

c = Color('ff00ff')
print(c.as_named())
#> magenta
print(c.as_hex())
#> #f0f
c2 = Color('green')
print(c2.as_rgb_tuple())
#> (0, 128, 0)
print(c2.original())
#> green
print(repr(Color('hsl(180, 100%, 50%)')))
#> Color('cyan', rgb=(0, 255, 255))


class Model(BaseModel):
    color: Color


print(Model(color='purple'))
#> color=Color('purple', rgb=(128, 0, 128))
try:
    Model(color='hello')
except ValidationError as e:
    print(e)
    """
    1 validation error for Model
    color
      value is not a valid color: string not recognised as a valid color
    (type=value_error.color; reason=string not recognised as a

Color 有下面一些方法:

  • original传递给 Color 的原始字符串或元组。
  • as_named返回一个命名的CSS3的颜色;如果设置了Alpha通道或不存在这种颜色,则会失败。如果提供 fallback=True,则回退到 as_hex。
  • as_hex返回格式为 #fff 或 #ffffff 的字符串; 如果设置了Alpha通道,则将包含4(或8)个十六进制值,例如 #7f33cc26 。
  • as_rgb如果设置了Alpha通道,则返回格式为 rgb(<red>, <green>, <blue>) 或 rgba(<red>, <green>, <blue>, <alpha>) 的字符串。
  • as_rgb_tuple以RGB(a) 格式返回3元组或4元组。 alpha 关键字参数可用于定义是否应包含alpha通道; 选项:True——始终包含,False——从不包含,None(默认值)——如果设置,则包含。
  • as_hslhsl(<hue deg>, <saturation %>, <lightness %>) 格式的字符串。如果设置了 alpha 通道,则为 hsl(<hue deg>, <saturation %>, <lightness %>, <alpha>)。
  • as_hsl_tuple以HSL(a)格式返回3元组或4元组。 alpha 关键字参数可用于定义是否应包含alpha通道; 选项:True——始终包含,False——从不包含,None(默认值)——如果设置,则包含。

Color 的 __str__ 方法返回 self.as_named(fallback=True)。

注意

as_hsl* 是指html和世界上大多数地方使用的色相,饱和度和亮度 “HSL”,而不是Python的 colorsys 中使用的 “HLS”。

3.2.3.3 Secret 类型

您可以使用 SecretStr 和 SecretBytes 数据类型来存储不想在日志记录或回溯中可见的敏感信息。SecretStr 和 SecretBytes 可以幂等初始化,也可以分别使用 str 或 bytes 字面量初始化。 在转换为json时,SecretStr 和 SecretBytes 的格式将为 '**********' 或 ''。

from pydantic import BaseModel, SecretStr, SecretBytes, ValidationError


class SimpleModel(BaseModel):
    password: SecretStr
    password_bytes: SecretBytes


sm = SimpleModel(password='IAmSensitive', password_bytes=b'IAmSensitiveBytes')

# Standard access methods will not display the secret
print(sm)
#> password=SecretStr('**********') password_bytes=SecretBytes(b'**********')
print(sm.password)
#> **********
print(sm.dict())
"""
{
    'password': SecretStr('**********'),
    'password_bytes': SecretBytes(b'**********'),
}
"""
print(sm.json())
#> {"password": "**********", "password_bytes": "**********"}

# Use get_secret_value method to see the secret's content.
print(sm.password.get_secret_value())
#> IAmSensitive
print(sm.password_bytes.get_secret_value())
#> b'IAmSensitiveBytes'

try:
    SimpleModel(password=[1, 2, 3], password_bytes=[1, 2, 3])
except ValidationError as e:
    print(e)
    """
    2 validation errors for SimpleModel
    password
      str type expected (type=type_error.str)
    password_bytes
      byte type expected (type=type_error.bytes)
    """


# If you want the secret to be dumped as plain-text using the json method,
# you can use json_encoders in the Config class.
class SimpleModelDumpable(BaseModel):
    password: SecretStr
    password_bytes: SecretBytes

    class Config:
        json_encoders = {
            SecretStr: lambda v: v.get_secret_value() if v else None,
            SecretBytes: lambda v: v.get_secret_value() if v else None,
        }


sm2 = SimpleModelDumpable(
    password='IAmSensitive', password_bytes=b'IAmSensitiveBytes'
)

# Standard access methods will not display the secret
print(sm2)
#> password=SecretStr('**********') password_bytes=SecretBytes(b'**********')
print(sm2.password)
#> **********
print(sm2.dict())
"""
{
    'password': SecretStr('**********'),
    'password_bytes': SecretBytes(b'**********'),
}
"""

# But the json method will
print(sm2.json())
#> {"password": "IAmSensitive", "password_bytes": "IAmSensitiveBytes"}
3.2.3.4 Json 类型

可以使用 Json 数据类型使pydantic加载原始JSON字符串。 还可以根据 Json 的参数化类型来选择将加载的对象解析为另一种类型:

from typing import List

from pydantic import BaseModel, Json, ValidationError


class SimpleJsonModel(BaseModel):
    json_obj: Json


class ComplexJsonModel(BaseModel):
    json_obj: Json[List[int]]


print(SimpleJsonModel(json_obj='{"b": 1}'))
#> json_obj={'b': 1}
print(ComplexJsonModel(json_obj='[1, 2, 3]'))
#> json_obj=[1, 2, 3]
try:
    ComplexJsonModel(json_obj=12)
except ValidationError as e:
    print(e)
    """
    1 validation error for ComplexJsonModel
    json_obj
      JSON object must be str, bytes or bytearray (type=type_error.json)
    """

try:
    ComplexJsonModel(json_obj='[a, b]')
except ValidationError as e:
    print(e)
    """
    1 validation error for ComplexJsonModel
    json_obj
      Invalid JSON (type=value_error.json)
    """

try:
    ComplexJsonModel(json_obj='["a", "b"]')
except ValidationError as e:
    print(e)
    """
    2 validation errors for ComplexJsonModel
    json_obj -> 0
      value is not a valid integer (type=type_error.integer)
    json_obj -> 1
      value is not a valid integer (type=type_error.integer)
    """
3.2.3.5 PaymentCardNumber

PaymentCardNumber 类型用于验证 支付卡 (例如贷记卡或信用卡)。

from datetime import date

from pydantic import BaseModel
from pydantic.types import PaymentCardBrand, PaymentCardNumber, constr


class Card(BaseModel):
    name: constr(strip_whitespace=True, min_length=1)
    number: PaymentCardNumber
    exp: date

    @property
    def brand(self) -> PaymentCardBrand:
        return self.number.brand

    @property
    def expired(self) -> bool:
        return self.exp < date.today()


card = Card(
    name='Georg Wilhelm Friedrich Hegel',
    number='4000000000000002',
    exp=date(2023, 9, 30),
)

assert card.number.brand == PaymentCardBrand.visa
assert card.number.bin == '400000'
assert card.number.last4 == '0002'
assert card.number.masked == '400000******0002'

基于BIN,PaymentCardBrand 可以是以下之一:

  • PaymentCardBrand.amex
  • PaymentCardBrand.mastercard
  • PaymentCardBrand.visa
  • PaymentCardBrand.other

实际验证验证卡号为:

  • 仅含数字的字符串
  • luhn 有效
  • 如果是 Amex、Mastercard 或 Visa,基于 BIN 长度正确;对于所有其他品牌,则为12至19位数字。

3.2.4 约束类型

可以使用 con* 类型函数来约束许多常见类型的值:

from decimal import Decimal

from pydantic import (
    BaseModel,
    NegativeFloat,
    NegativeInt,
    PositiveFloat,
    PositiveInt,
    conbytes,
    condecimal,
    confloat,
    conint,
    conlist,
    conset,
    constr,
    Field,
)


class Model(BaseModel):
    short_bytes: conbytes(min_length=2, max_length=10)
    strip_bytes: conbytes(strip_whitespace=True)

    short_str: constr(min_length=2, max_length=10)
    regex_str: constr(regex=r'^apple (pie|tart|sandwich)$')
    strip_str: constr(strip_whitespace=True)

    big_int: conint(gt=1000, lt=1024)
    mod_int: conint(multiple_of=5)
    pos_int: PositiveInt
    neg_int: NegativeInt

    big_float: confloat(gt=1000, lt=1024)
    unit_interval: confloat(ge=0, le=1)
    mod_float: confloat(multiple_of=0.5)
    pos_float: PositiveFloat
    neg_float: NegativeFloat

    short_list: conlist(int, min_items=1, max_items=4)
    short_set: conset(int, min_items=1, max_items=4)

    decimal_positive: condecimal(gt=0)
    decimal_negative: condecimal(lt=0)
    decimal_max_digits_and_places: condecimal(max_digits=2, decimal_places=2)
    mod_decimal: condecimal(multiple_of=Decimal('0.25'))

    bigger_int: int = Field(..., gt=10000)

其中,Field 引用 [字段函数](# 3.5.1 字段定制)。

3.2.5 Strict类型

您可以使用 StrictStr,StrictInt,StrictFloat 和 StrictBool 类型来防止兼容类型强制转换。 仅当已验证的值属于相应类型或该类型的子类型时,这些类型才会通过验证。 此行为也通过 ConstrainedStr,ConstrainedFloat 和 ConstrainedInt 类的 strict 字段公开,并且可以与许多复杂的验证规则结合使用。

以下警告适用:

  • StrictInt (和 ConstrainedInt 的strict 选项) 不会接受 bool 类型,即使在 Python 中 bool 是 int 的子类。其他子类则会被接受。
  • StrictFloat (和 ConstrainedFloat 的 strict 选项) 不会接受 int。
from pydantic import BaseModel, StrictBool, StrictInt, ValidationError, confloat


class StrictIntModel(BaseModel):
    strict_int: StrictInt


try:
    StrictIntModel(strict_int=3.14159)
except ValidationError as e:
    print(e)
    """
    1 validation error for StrictIntModel
    strict_int
      value is not a valid integer (type=type_error.integer)
    """


class ConstrainedFloatModel(BaseModel):
    constrained_float: confloat(strict=True, ge=0.0)


try:
    ConstrainedFloatModel(constrained_float=3)
except ValidationError as e:
    print(e)
    """
    1 validation error for ConstrainedFloatModel
    constrained_float
      value is not a valid float (type=type_error.float)
    """

try:
    ConstrainedFloatModel(constrained_float=-1.23)
except ValidationError as e:
    print(e)
    """
    1 validation error for ConstrainedFloatModel
    constrained_float
      ensure this value is greater than or equal to 0.0
    (type=value_error.number.not_ge; limit_value=0.0)
    """


class StrictBoolModel(BaseModel):
    strict_bool: StrictBool


try:
    StrictBoolModel(strict_bool='False')
except ValidationError as e:
    print(str(e))
    """
    1 validation error for StrictBoolModel
    strict_bool
      value is not a valid boolean (type=value_error.strictbool)

3.2.6 ByteSize

您可以使用 ByteSize 数据类型将字节字符串表示形式转换为原始字节,并打印出人类可读的字节版本。

注意

注意,1b 将会被解析为 “1 bytes”,而不是 “1 bit”。

from pydantic import BaseModel, ByteSize


class MyModel(BaseModel):
    size: ByteSize


print(MyModel(size=52000).size)
#> 52000
print(MyModel(size='3000 KiB').size)
#> 3072000

m = MyModel(size='50 PB')
print(m.size.human_readable())
#> 44.4PiB
print(m.size.human_readable(decimal=True))
#> 50.0PB

print(m.size.to('TiB'))
#> 45474.73508864641

3.2.7 自定义数据类型

你也可以定义你自己的自定义数据类型。有多种方法可以达到这个目的。

3.2.7.1 带有 __get_validators__ 的类

使用带有 __get_validators__ 类方法的自定义类。它将被调用来使验证器解析和验证输入数据。

提示

这些验证器与[验证器](# 3.3 验证器) 中的验证器具有相同的语义,因此,你可以声明参数 config、field 等。

import re
from pydantic import BaseModel

# https://en.wikipedia.org/wiki/Postcodes_in_the_United_Kingdom#Validation
post_code_regex = re.compile(
    r'(?:'
    r'([A-Z]{1,2}[0-9][A-Z0-9]?|ASCN|STHL|TDCU|BBND|[BFS]IQQ|PCRN|TKCA) ?'
    r'([0-9][A-Z]{2})|'
    r'(BFPO) ?([0-9]{1,4})|'
    r'(KY[0-9]|MSR|VG|AI)[ -]?[0-9]{4}|'
    r'([A-Z]{2}) ?([0-9]{2})|'
    r'(GE) ?(CX)|'
    r'(GIR) ?(0A{2})|'
    r'(SAN) ?(TA1)'
    r')'
)


class PostCode(str):
    """
    Partial UK postcode validation. Note: this is just an example, and is not
    intended for use in production; in particular this does NOT guarantee
    a postcode exists, just that it has a valid format.
    """

    @classmethod
    def __get_validators__(cls):
        # one or more validators may be yielded which will be called in the
        # order to validate the input, each validator will receive as an input
        # the value returned from the previous validator
        yield cls.validate

    @classmethod
    def __modify_schema__(cls, field_schema):
        # __modify_schema__ should mutate the dict it receives in place,
        # the returned value will be ignored
        field_schema.update(
            # simplified regex here for brevity, see the wikipedia link above
            pattern='^[A-Z]{1,2}[0-9][A-Z0-9]? ?[0-9][A-Z]{2}$',
            # some example postcodes
            examples=['SP11 9DG', 'w1j7bu'],
        )

    @classmethod
    def validate(cls, v):
        if not isinstance(v, str):
            raise TypeError('string required')
        m = post_code_regex.fullmatch(v.upper())
        if not m:
            raise ValueError('invalid postcode format')
        # you could also return a string here which would mean model.post_code
        # would be a string, pydantic won't care but you could end up with some
        # confusion since the value's type won't match the type annotation
        # exactly
        return cls(f'{m.group(1)} {m.group(2)}')

    def __repr__(self):
        return f'PostCode({super().__repr__()})'


class Model(BaseModel):
    post_code: PostCode


model = Model(post_code='sw8 5el')
print(model)
#> post_code=PostCode('SW8 5EL')
print(model.post_code)
#> SW8 5EL
print(Model.schema())
"""
{
    'title': 'Model',
    'type': 'object',
    'properties': {
        'post_code': {
            'title': 'Post Code',
            'pattern': '^[A-Z]{1,2}[0-9][A-Z0-9]? ?[0-9][A-Z]{2}$',
            'examples': ['SP11 9DG', 'w1j7bu'],
            'type': 'string',
        },
    },
    'required': ['post_code'],
}
"""

使用 constr(regex=...) 可以实现类似的验证,只是该值不会用空格格式化,该模式(schema)将仅包括完整模式(pattern),并且返回的值将是一个普通字符串。

有关如何生成模型的模式的更多详细信息,请参见 [模式](# 3.5 模式)。

3.2.7.2 允许任意类型

您可以使用模型配置中的 arbitrary_types_allowed 配置来允许任意类型。

from pydantic import BaseModel, ValidationError


# This is not a pydantic model, it's an arbitrary class
class Pet:
    def __init__(self, name: str):
        self.name = name


class Model(BaseModel):
    pet: Pet
    owner: str

    class Config:
        arbitrary_types_allowed = True


pet = Pet(name='Hedwig')
# A simple check of instance type is used to validate the data
model = Model(owner='Harry', pet=pet)
print(model)
#> pet=<types_arbitrary_allowed.Pet object at 0x7fcb902cee50> owner='Harry'
print(model.pet)
#> <types_arbitrary_allowed.Pet object at 0x7fcb902cee50>
print(model.pet.name)
#> Hedwig
print(type(model.pet))
#> <class 'types_arbitrary_allowed.Pet'>
try:
    # If the value is not an instance of the type, it's invalid
    Model(owner='Harry', pet='Hedwig')
except ValidationError as e:
    print(e)
    """
    1 validation error for Model
    pet
      instance of Pet expected (type=type_error.arbitrary_type;
    expected_arbitrary_type=Pet)
    """
# Nothing in the instance of the arbitrary type is checked
# Here name probably should have been a str, but it's not validated
pet2 = Pet(name=42)
model2 = Model(owner='Harry', pet=pet2)
print(model2)
#> pet=<types_arbitrary_allowed.Pet object at 0x7fcb902ce4f0> owner='Harry'
print(model2.pet)
#> <types_arbitrary_allowed.Pet object at 0x7fcb902ce4f0>
print(model2.pet.name)
#> 42
print(type(model2.pet))
#> <class 'types_arbitrary_allowed.Pet'>
3.2.2.3 泛型类作为类型

这是一开始可能不需要的高级技术。 在大多数情况下,使用标准的pytantic模型可能会很好。

可以使用泛型类作为字段类型并使用 __get_validators__ 基于"类型参数" (或子类型)执行自定义验证。

如果您要用作子类型的泛型类具有类方法(classmethod) __get_validators__,则无需使用 arbitrary_types_allowed 即可工作。

因为可以声明接收当前 field 的验证器,所以可以提取 sub_fields (从泛型类类型参数中) 并使用它们验证数据。

from pydantic import BaseModel, ValidationError
from pydantic.fields import ModelField
from typing import TypeVar, Generic

AgedType = TypeVar('AgedType')
QualityType = TypeVar('QualityType')


# This is not a pydantic model, it's an arbitrary generic class
class TastingModel(Generic[AgedType, QualityType]):
    def __init__(self, name: str, aged: AgedType, quality: QualityType):
        self.name = name
        self.aged = aged
        self.quality = quality

    @classmethod
    def __get_validators__(cls):
        yield cls.validate

    @classmethod
    # You don't need to add the "ModelField", but it will help your
    # editor give you completion and catch errors
    def validate(cls, v, field: ModelField):
        if not isinstance(v, cls):
            # The value is not even a TastingModel
            raise TypeError('Invalid value')
        if not field.sub_fields:
            # Generic parameters were not provided so we don't try to validate
            # them and just return the value as is
            return v
        aged_f = field.sub_fields[0]
        quality_f = field.sub_fields[1]
        errors = []
        # Here we don't need the validated value, but we want the errors
        valid_value, error = aged_f.validate(v.aged, {}, loc='aged')
        if error:
            errors.append(error)
        # Here we don't need the validated value, but we want the errors
        valid_value, error = quality_f.validate(v.quality, {}, loc='quality')
        if error:
            errors.append(error)
        if errors:
            raise ValidationError(errors, cls)
        # Validation passed without errors, return the same instance received
        return v


class Model(BaseModel):
    # for wine, "aged" is an int with years, "quality" is a float
    wine: TastingModel[int, float]
    # for cheese, "aged" is a bool, "quality" is a str
    cheese: TastingModel[bool, str]
    # for thing, "aged" is a Any, "quality" is Any
    thing: TastingModel


model = Model(
    # This wine was aged for 20 years and has a quality of 85.6
    wine=TastingModel(name='Cabernet Sauvignon', aged=20, quality=85.6),
    # This cheese is aged (is mature) and has "Good" quality
    cheese=TastingModel(name='Gouda', aged=True, quality='Good'),
    # This Python thing has aged "Not much" and has a quality "Awesome"
    thing=TastingModel(name='Python', aged='Not much', quality='Awesome'),
)
print(model)
"""
wine=<types_generics.TastingModel object at 0x7fcb903271f0>
cheese=<types_generics.TastingModel object at 0x7fcb903272e0>
thing=<types_generics.TastingModel object at 0x7fcb903276a0>
"""
print(model.wine.aged)
#> 20
print(model.wine.quality)
#> 85.6
print(model.cheese.aged)
#> True
print(model.cheese.quality)
#> Good
print(model.thing.aged)
#> Not much
try:
    # If the values of the sub-types are invalid, we get an error
    Model(
        # For wine, aged should be an int with the years, and quality a float
        wine=TastingModel(name='Merlot', aged=True, quality='Kinda good'),
        # For cheese, aged should be a bool, and quality a str
        cheese=TastingModel(name='Gouda', aged='yeah', quality=5),
        # For thing, no type parameters are declared, and we skipped validation
        # in those cases in the Assessment.validate() function
        thing=TastingModel(name='Python', aged='Not much', quality='Awesome'),
    )
except ValidationError as e:
    print(e)
    """
    2 validation errors for Model
    wine -> quality
      value is not a valid float (type=type_error.float)
    cheese -> aged
      value could not be parsed to a boolean (type=type_error.bool)
    """

3.3 验证器

可以使用 validator 装饰器实现对象之间的自定义验证和复杂关系。

from pydantic import BaseModel, ValidationError, validator


class UserModel(BaseModel):
    name: str
    username: str
    password1: str
    password2: str

    @validator('name')
    def name_must_contain_space(cls, v):
        if ' ' not in v:
            raise ValueError('must contain a space')
        return v.title()

    @validator('password2')
    def passwords_match(cls, v, values, **kwargs):
        if 'password1' in values and v != values['password1']:
            raise ValueError('passwords do not match')
        return v

    @validator('username')
    def username_alphanumeric(cls, v):
        assert v.isalnum(), 'must be alphanumeric'
        return v


user = UserModel(
    name='samuel colvin',
    username='scolvin',
    password1='zxcvbn',
    password2='zxcvbn',
)
print(user)
#> name='Samuel Colvin' username='scolvin' password1='zxcvbn' password2='zxcvbn'

try:
    UserModel(
        name='samuel',
        username='scolvin',
        password1='zxcvbn',
        password2='zxcvbn2',
    )
except ValidationError as e:
    print(e)
    """
    2 validation errors for UserModel
    name
      must contain a space (type=value_error)
    password2
      passwords do not match (type=value_error)
    """

验证器上应该注意的几件事:

  • 验证器是类方法(class method),所以其接收到的第一个参数值是 UserModel 类,而不是 UserModel 类的实例。
  • 第二个参数总是要验证的字段值。
  • 可以将下面参数的任意子集添加到签名中(参数名必须匹配)
  • values 之前已验证字段的字段名到字段值的映射。
  • config 模型配置。
  • field 要验证的字段。
  • **kwargs 如果提供,则包含上面这些参数中未在签名中显式列出的参数。
  • 验证器应该返回解析的值或者引发 ValueError、TypeError 或 AssertionError (可以使用 assert 语句)。

警告

如果你使用 assert 语句,那么请注意, 带有 -O 优化标志 的 Python 将会禁用 assert 语句,导致验证器停止工作。

  • 当验证器依赖于其他值时,你应该知道:
  • 验证按照字段被定义的顺序完成。例如,在上面的示例中,password2 可以访问 password1 (和 name),但 password1 不能访问 password2。关于字段如何排序的详细信息,请参考 [字段排序](# 3.1.11 字段排序)。
  • 如果在其他字段上验证失败 (或那个字段缺失),则它不会被包含在 values 中,因此,在示例中使用了 if 'password1' in values and ... 来进行判断。

3.3.1 Pre 和 per-item 验证器

验证器可以做更复杂的事情:

from typing import List
from pydantic import BaseModel, ValidationError, validator


class DemoModel(BaseModel):
    square_numbers: List[int] = []
    cube_numbers: List[int] = []

    # '*' is the same as 'cube_numbers', 'square_numbers' here:
    @validator('*', pre=True)
    def split_str(cls, v):
        if isinstance(v, str):
            return v.split('|')
        return v

    @validator('cube_numbers', 'square_numbers')
    def check_sum(cls, v):
        if sum(v) > 42:
            raise ValueError('sum of numbers greater than 42')
        return v

    @validator('square_numbers', each_item=True)
    def check_squares(cls, v):
        assert v ** 0.5 % 1 == 0, f'{v} is not a square number'
        return v

    @validator('cube_numbers', each_item=True)
    def check_cubes(cls, v):
        # 64 ** (1 / 3) == 3.9999999999999996 (!)
        # this is not a good way of checking cubes
        assert v ** (1 / 3) % 1 == 0, f'{v} is not a cubed number'
        return v


print(DemoModel(square_numbers=[1, 4, 9]))
#> square_numbers=[1, 4, 9] cube_numbers=[]
print(DemoModel(square_numbers='1|4|16'))
#> square_numbers=[1, 4, 16] cube_numbers=[]
print(DemoModel(square_numbers=[16], cube_numbers=[8, 27]))
#> square_numbers=[16] cube_numbers=[8, 27]
try:
    DemoModel(square_numbers=[1, 4, 2])
except ValidationError as e:
    print(e)
    """
    1 validation error for DemoModel
    square_numbers -> 2
      2 is not a square number (type=assertion_error)
    """

try:
    DemoModel(cube_numbers=[27, 27])
except ValidationError as e:
    print(e)
    """
    1 validation error for DemoModel
    cube_numbers
      sum of numbers greater than 42 (type=value_error)
    """

需要注意的事情:

  • 可以通过给单个验证器传递多个字段名称将其应用于多个字段。
  • 可以通过给单个验证器传递特殊的 * 值将其应用于所有字段。
  • 关键字参数 pre 将会导致该验证器的调用优先于其他验证器。
  • 传递 each_item=True 将会导致验证器应用于可迭代对象中的每个值(例如,List、Dict 和 Set 等),而不是整个对象。

3.3.2 子类验证器和 each_item

如果将验证器与引用父类上 List 类型字段的子类一起使用,那么 each_item=True 将导致验证器无法运行;相反,必须以编程方式遍历该列表。

from typing import List
from pydantic import BaseModel, ValidationError, validator


class ParentModel(BaseModel):
    names: List[str]


class ChildModel(ParentModel):
    @validator('names', each_item=True)
    def check_names_not_empty(cls, v):
        assert v != '', 'Empty strings are not allowed.'
        return v


# This will NOT raise a ValidationError because the validator was not called
try:
    child = ChildModel(names=['Alice', 'Bob', 'Eve', ''])
except ValidationError as e:
    print(e)
else:
    print('No ValidationError caught.')
    #> No ValidationError caught.


class ChildModel2(ParentModel):
    @validator('names')
    def check_names_not_empty(cls, v):
        for name in v:
            assert name != '', 'Empty strings are not allowed.'
        return v


try:
    child = ChildModel2(names=['Alice', 'Bob', 'Eve', ''])
except ValidationError as e:
    print(e)
    """
    1 validation error for ChildModel2
    names
      Empty strings are not allowed. (type=assertion_error)
    """

3.3.3 Always验证

出于性能原因,在没有提供值时,默认情况下不会为字段调用验证器。然而,在某些情况下,调用验证器可能是有用的或必需的,例如,设置一个动态默认值。

from datetime import datetime

from pydantic import BaseModel, validator


class DemoModel(BaseModel):
    ts: datetime = None

    @validator('ts', pre=True, always=True)
    def set_ts_now(cls, v):
        return v or datetime.now()


print(DemoModel())
#> ts=datetime.datetime(2020, 10, 28, 20, 3, 33, 424317)
print(DemoModel(ts='2017-11-08T14:00'))
#> ts=datetime.datetime(2017, 11, 8, 14, 0)

注意,最好将 always=True 和 pre=True 一起使用,否则,当 always=True 时,pydantic会尝试对默认值 None 进行验证,从而导致错误。

3.3.4 复用验证器

有时,你会想要在多个 字段/模型 上使用相同的验证器(例如,规范化一些输入数据)。最容易想到的方法就是编写一个单独的函数,然后从多个装饰器进行调用。显然,这需要大量的重复和样板代码。为了避免这种情况,已经在 v1.2 中为 pydantic.validator 添加了 allow_reuse 参数(默认值为 False):

from pydantic import BaseModel, validator


def normalize(name: str) -> str:
    return ' '.join((word.capitalize()) for word in name.split(' '))


class Producer(BaseModel):
    name: str

    # validators
    _normalize_name = validator('name', allow_reuse=True)(normalize)


class Consumer(BaseModel):
    name: str

    # validators
    _normalize_name = validator('name', allow_reuse=True)(normalize)


jane_doe = Producer(name='JaNe DOE')
john_doe = Consumer(name='joHN dOe')
assert jane_doe.name == 'Jane Doe'
assert john_doe.name == 'John Doe'

很明显,重复减少了,模型再次变得几乎是声明性的。

提示

如果您有很多字段需要验证,那么通常有必要定义一个帮助函数,使用该函数可以避免一次又一次地设置 allow_reuse=True。

例如,对于上面的例子,可以将代码改写为:

from pydantic import validator, BaseModel @validator('name', allow_reuse=True) def normalize(name: str) -> str: return ' '.join((word.capitalize()) for word in name.split(' ')) class Producer(BaseModel): name: str # validators _normalize_name = normalize class Consumer(BaseModel): name: str # validators _normalize_name = normalize jane_doe = Producer(name='JaNe DOE') john_doe = Consumer(name='joHN dOe') assert jane_doe.name == 'Jane Doe' assert john_doe.name == 'John Doe'

3.3.5 根验证器

验证也可以在整个模型的数据上执行。

from pydantic import BaseModel, ValidationError, root_validator


class UserModel(BaseModel):
    username: str
    password1: str
    password2: str

    @root_validator(pre=True)
    def check_card_number_omitted(cls, values):
        assert 'card_number' not in values, 'card_number should not be included'
        return values

    @root_validator
    def check_passwords_match(cls, values):
        pw1, pw2 = values.get('password1'), values.get('password2')
        if pw1 is not None and pw2 is not None and pw1 != pw2:
            raise ValueError('passwords do not match')
        return values


print(UserModel(username='scolvin', password1='zxcvbn', password2='zxcvbn'))
#> username='scolvin' password1='zxcvbn' password2='zxcvbn'
try:
    UserModel(username='scolvin', password1='zxcvbn', password2='zxcvbn2')
except ValidationError as e:
    print(e)
    """
    1 validation error for UserModel
    __root__
      passwords do not match (type=value_error)
    """

try:
    UserModel(
        username='scolvin',
        password1='zxcvbn',
        password2='zxcvbn',
        card_number='1234',
    )
except ValidationError as e:
    print(e)
    """
    1 validation error for UserModel
    __root__
      card_number should not be included (type=assertion_error)
    """

与字段验证器一样,根验证器可以具有 pre=True,在这种情况下,它们会在字段验证发生之前被调用(并与原始输入数据一起提供),或者 pre=False (默认情况下),在这种情况下,它们会在字段验证之后被调用。

如果 pre=True 时根验证器引发错误,则不会进行字段验证。与字段验证器一样,当 pre=False 时,即使之前的验证器失败,默认情况下也会调用根验证器;可以通过为验证器设置 skip_on_failure=True 关键字参数来改变这种行为。values 参数将是一个字典,其中包含已通过字段验证的值和适用的字段默认值。

3.3.6 字段检查

在创建类时,将检查验证器,以确认它们指定的字段在模型中实际存在。

但有时这是不希望看到的:例如,如果您定义了一个验证器来验证继承模型上的字段。在这种情况下,应该在验证器上设置 check_fields=False。

3.3.7 Dataclass 验证器

验证器也可以与pydantic 的 dataclasses 一起工作:

from datetime import datetime

from pydantic import validator
from pydantic.dataclasses import dataclass


@dataclass
class DemoDataclass:
    ts: datetime = None

    @validator('ts', pre=True, always=True)
    def set_ts_now(cls, v):
        return v or datetime.now()


print(DemoDataclass())
#> DemoDataclass(ts=datetime.datetime(2020, 11, 1, 15, 49, 0, 182639))
print(DemoDataclass(ts='2017-11-08T14:00'))
#> DemoDataclass(ts=datetime.datetime(2017, 11, 8, 14, 0))

3.4 模型配置

Pydantic 的行为可以通过模型上的 Config 类控制。

选项:

  • title声成的 JSON 模式的标题。
  • anystr_strip_whitespace是否移除 str 和 bytes 类型中前导和尾随的空白字符(默认值为 False)。
  • min_anystr_lengthstr 和 bytes 类型的最小长度 (默认为 0)。
  • max_anystr_lengthstr 和 bytes 类型的最小长度 (默认为 2 ** 16)。
  • validate_all是否验证字段的默认值 (默认为 False)。
  • extra在模型初始化时是否忽略、允许或禁止额外的属性。可接受字符串值 ignore、allow 或 forbid 以及 Extra 枚举 (例如, Extra.ignore)。如果模型包含了额外的属性,则 forbid 将会导致验证失败。ignore 将静默忽略任何额外属性。allow 将属性分配给模型。
  • allow_mutation模型是否是伪不可变的。例如,是否允许 __setattr__ (默认为 True)。
  • use_enum_values是否使用枚举的 value 属性而不是原始枚举填充模型。如果之后想要序列化 model.dict() ,这可能会很有用 (默认值为 False)。
  • fields包含每个字段的模式信息的字典; 这等效于使用 [Field 类](# 3.5 模式) (默认为 None)。
  • validate_assignment是否对属性的赋值执行验证 (默认为 False)。
  • allow_population_by_field_name是否可以用模型属性给出的名称填充别名字段,以及别名 (默认为 False )。

注意

这个配置设置的名称在v1.0中从 allow_population_by_alias 更改为 allow_population_by_field_name。

  • error_msg_templates用于覆盖默认错误消息模板的字典。传入一个字典,其中的键与您想要覆盖的错误消息相匹配 (默认为 {})。
  • arbitrary_types_allowed是否允许字段使用任意用户类型(只需检查值是否为该类型的实例即可对它们进行验证)。 如果为 False,则会在模型声明时引发RuntimeError(默认为 False)。 请参阅[字段类型](# 3.2 字段类型)中的示例。
  • orm_mode是否允许使用 [ORM 模式](# 3.1.3 ORM 模式)。
  • getter_dict一个自定义类 (应该继承自 GetterDict),用于分解ORM类进行验证,并与 orm_mode 一起使用
  • alias_generator接受字段名并返回其别名的可调用对象。
  • keep_untouched模型的默认值的类型元组 (例如,描述符),该默认值在模型创建期间不应更改,也不会包含在模型模式中。 注意:这意味着模型上具有此类型的默认值的属性 (而不是此类型的注释) 将被保留。
  • schema_extra用于 拓展/更新 生成的 JSON 模式的字典,或用于对其进行后处理(post-process)的可调用对象。参见 [模式定制](# 3.5.5 模式定制)。
  • json_loads用于解码 JSON 的自定义函数。参见 [自定义 JSON反序列化](# 3.6.4.3 自定义 JSON反序列化)。
  • json_dumps用于编码 JSON的自定义函数。参见 [自定义 JSON反序列化](# 3.6.4.3 自定义 JSON反序列化)。
  • json_encoders用于自定义类型被编码成JSON的方式的字典。参见 [JSON 序列化](# 3.6.4 model.json(…))。
  • underscore_attrs_are_private是否将任何下划线非类 (non-class) 变量属性当做私有属性,或让它们保持原样。参见 [私有模型属性](# 3.1.14 私有模型属性)。
from pydantic import BaseModel, ValidationError


class Model(BaseModel):
    v: str

    class Config:
        max_anystr_length = 10
        error_msg_templates = {
            'value_error.any_str.max_length': 'max_length:{limit_value}',
        }


try:
    Model(v='x' * 20)
except ValidationError as e:
    print(e)
    """
    1 validation error for Model
    v
      max_length:10 (type=value_error.any_str.max_length; limit_value=10)
    """

类似的,如果使用 @dataclass 装饰器:

from datetime import datetime

from pydantic import ValidationError
from pydantic.dataclasses import dataclass


class MyConfig:
    max_anystr_length = 10
    validate_assignment = True
    error_msg_templates = {
        'value_error.any_str.max_length': 'max_length:{limit_value}',
    }


@dataclass(config=MyConfig)
class User:
    id: int
    name: str = 'John Doe'
    signup_ts: datetime = None


user = User(id='42', signup_ts='2032-06-21T12:00')
try:
    user.name = 'x' * 20
except ValidationError as e:
    print(e)
    """
    1 validation error for User
    name
      max_length:10 (type=value_error.any_str.max_length; limit_value=10)
    """

3.4.1 别名生成器

如果数据源字段名称与您的代码样式不匹配 (例如CamelCase字段),则可以使用 alias_generator 自动生成别名:

from pydantic import BaseModel


def to_camel(string: str) -> str:
    return ''.join(word.capitalize() for word in string.split('_'))


class Voice(BaseModel):
    name: str
    language_code: str

    class Config:
        alias_generator = to_camel


voice = Voice(Name='Filiz', LanguageCode='tr-TR')
print(voice.language_code)
#> tr-TR
print(voice.dict(by_alias=True))
#> {'Name': 'Filiz', 'LanguageCode': 'tr-TR'}

这里,“camel case” 指的是 “upper camel case”,又称 “pascal case”,例如 CamelCase。如果您希望使用 “lower camel case”,例如 camelCase,那么修改上面的 to_camel 函数应该很简单。

3.4.2 别名优先

警告

别名优先级逻辑在v1.4中更改,以解决以前版本中的错误和意外行为。在某些情况下,这可能表示一个中断的更改,详细信息请参阅#1178和下面的优先顺序。

在一个字段的别名可能被定义在多个地方的情况下,选择的值按如下规则确定(按优先级降序):

  • 在模型上直接通过 Field(..., alias=<alias>) 设置。
  • 在模型上的 Config.fields 中定义。
  • 在父模型上通过 Field(..., alias=<alias>)。
  • 在父模型上的 Config.fields 中定义。
  • 由 alias_generator 生成,无论它是在模型上还是在父模型上。

注意

这意味着在子模型上定义的 alias_generator 不优先于在父模型的字段上定义的别名。

示例:

from pydantic import BaseModel, Field


class Voice(BaseModel):
    name: str = Field(None, alias='ActorName')
    language_code: str = None
    mood: str = None


class Character(Voice):
    act: int = 1

    class Config:
        fields = {'language_code': 'lang'}

        @classmethod
        def alias_generator(cls, string: str) -> str:
            # this is the same as `alias_generator = to_camel` above
            return ''.join(word.capitalize() for word in string.split('_'))


print(Character.schema(by_alias=True))
"""
{
    'title': 'Character',
    'type': 'object',
    'properties': {
        'ActorName': {'title': 'Actorname', 'type': 'string'},
        'lang': {'title': 'Lang', 'type': 'string'},
        'Mood': {'title': 'Mood', 'type': 'string'},
        'Act': {'title': 'Act', 'default': 1, 'type': 'integer'},
    },
}
"""

3.5 模式

Pydantic 允许从模型自动创建 JSON 模式:

from enum import Enum
from pydantic import BaseModel, Field


class FooBar(BaseModel):
    count: int
    size: float = None


class Gender(str, Enum):
    male = 'male'
    female = 'female'
    other = 'other'
    not_given = 'not_given'


class MainModel(BaseModel):
    """
    This is the description of the main model
    """

    foo_bar: FooBar = Field(...)
    gender: Gender = Field(None, alias='Gender')
    snap: int = Field(
        42,
        title='The Snap',
        description='this is the value of snap',
        gt=30,
        lt=50,
    )

    class Config:
        title = 'Main'


# this is equivalent to json.dumps(MainModel.schema(), indent=2):
print(MainModel.schema_json(indent=2))

输出如下:

{
  "title": "Main",
  "description": "This is the description of the main model",
  "type": "object",
  "properties": {
    "foo_bar": {
      "$ref": "#/definitions/FooBar"
    },
    "Gender": {
      "$ref": "#/definitions/Gender"
    },
    "snap": {
      "title": "The Snap",
      "description": "this is the value of snap",
      "default": 42,
      "exclusiveMinimum": 30,
      "exclusiveMaximum": 50,
      "type": "integer"
    }
  },
  "required": [
    "foo_bar"
  ],
  "definitions": {
    "FooBar": {
      "title": "FooBar",
      "type": "object",
      "properties": {
        "count": {
          "title": "Count",
          "type": "integer"
        },
        "size": {
          "title": "Size",
          "type": "number"
        }
      },
      "required": [
        "count"
      ]
    },
    "Gender": {
      "title": "Gender",
      "description": "An enumeration.",
      "enum": [
        "male",
        "female",
        "other",
        "not_given"
      ],
      "type": "string"
    }
  }
}

生成的模式符合以下规范:

  • JSON Schema Core
  • JSON Schema Validation
  • OpenAPI

BaseModel.schema 将会返回模式字典,而 BaseModel.schema_json 将会返回那个字典的 JSON 字符串表示。

根据规范,使用的子模型被添加到 definitions JSON属性并被引用。

所有子模型(及其子模型)的模式都直接放在一个顶级 definitions JSON键中,以便于重用和引用。

经过修改 (通过 Field 类) 的 “子模型”,如自定义标题、描述或默认值,被递归地包括而不是引用。

模型的 description 取自类的文档字符串或 Field 类的 description 参数。

默认情况下使用别名作为键来生成模式,但是可以使用模型属性名而不是调用 MainModel.schema/schema_json(by_alias=False) 来生成模式。

使用 ref_template 关键字参数调用 schema() 或 schema_json() 可以改变 $ref (上面的 #/definitions/FooBar ) 的格式。

ApplePie.schema(ref_template='/schemas/{model}.json#/') ,这里,将会使用 str.format 将 {model} 替换为模型名称。

3.5.1 字段定制

此外,还可以使用 Field 函数提供关于字段和验证的额外信息。它有下面一些参数:

  • default该参数为位置参数。表示字段的默认值。因为 Field 替换字段的默认值,所以可以使用第一个参数设置默认值。使用省略号表示字段是必需的。
  • default_factory一个零参数的可调用对象,当需要该字段的默认值时将调用它。除了其他用途外,它还可用于设置动态默认值。禁止同时设置default 和 default_factory。
  • alias字段的公开名称。
  • title如果忽略,则使用 filed_name.title()。
  • description如果忽略并且注解是子模型,将会使用子模型的文档字符串。
  • const如果该参数出现,则必须与字段的默认值相同。
  • gt对于数值值 (int,float,Decimal),将向 JSON 模式添加一个 “大于” 验证和一个 exclusiveMinimum 注解。
  • ge对于数值值 ,将向 JSON 模式添加一个 “大于等于” 验证和一个 minimum 注解。
  • lt对于数值值 ,将向 JSON 模式添加一个"小于" 验证和一个 exclusiveMaximum 注解。
  • le对于数值值 ,将向 JSON 模式添加一个 “小于等于” 验证和一个 maximum 注解。
  • multiple_of对于数值值,将向 JSON 模式添加一个 “倍数” 验证和一个 multipleOf 注解。
  • min_items对于列表值,将向 JSON 模式添加相应的验证和 minItems 注解。
  • max_items对于列表值,将向 JSON 模式添加相应的验证和 maxItems 注解。
  • min_length对于字符串值,将向 JSON 模式添加相应的验证和 minLength 注解。
  • max_length对于字符串值,将向 JSON 模式添加相应的验证和 maxLength 注解。
  • regex对于字符串值,将向 JSON 模式添加一个从传递的字符串生成的正则表达式验证和一个 pattern 注解。

注意

pydantic使用 re.match 验证字符串,该方法将正则表达式视为隐式锚定在开始处。相反,JSON模式验证器将模式关键字视为隐式非锚定的,这更像 re.search 所做的。

为了互操作性,根据你想要的行为,要么显式地用 ^ 锚定你的正则表达式 (例如使用 ^foo 来匹配任何以 foo 开头的字符串),要么使用 .*? 显式地允许任意的前缀用 (例如 .*?foo 匹配任何包含子字符串 foo 的字符串)。

有关在v2中对pydantic行为可能进行的更改的讨论,请参阅#1631。

  • **任何其他关键字参数 (例如 example) 都将逐字添加到字段的模式中。

[配置类](# 3.4 模型配置) 的 fields 属性可以用来设置上面除 default 之外的所有参数,而不是使用 Field。

3.5.1.1 未强制执行的字段约束

如果 pydantic 找到未强制执行的约束,将引发错误。如果要强制约束出现在模式中,即使解析时没有检查该约束,可以将 Field() 的可变参数与原始模式属性名称一起使用:

from pydantic import BaseModel, Field, PositiveInt

try:
    # this won't work since PositiveInt takes precedence over the
    # constraints defined in Field meaning they're ignored
    class Model(BaseModel):
        foo: PositiveInt = Field(..., lt=10)
except ValueError as e:
    print(e)
    """
    On field "foo" the following field constraints are set but not enforced:
    lt.
    For more details see https://pydantic-
    docs.helpmanual.io/usage/schema/#unenforced-field-constraints
    """


# but you can set the schema attribute directly:
# (Note: here exclusiveMaximum will not be enforce)
class Model(BaseModel):
    foo: PositiveInt = Field(..., exclusiveMaximum=10)


print(Model.schema())
"""
{
    'title': 'Model',
    'type': 'object',
    'properties': {
        'foo': {
            'title': 'Foo',
            'exclusiveMaximum': 10,
            'exclusiveMinimum': 0,
            'type': 'integer',
        },
    },
    'required': ['foo'],
}
"""


# if you find yourself needing this, an alternative is to declare
# the constraints in Field (or you could use conint())
# here both constraints will be enforced:
class Model(BaseModel):
    # Here both constraints will be applied and the schema
    # will be generated correctly
    foo: int = Field(..., gt=0, lt=10)


print(Model.schema())
"""
{
    'title': 'Model',
    'type': 'object',
    'properties': {
        'foo': {
            'title': 'Foo',
            'exclusiveMinimum': 0,
            'exclusiveMaximum': 10,
            'type': 'integer',
        },
    },
    'required': ['foo'],
}
"""

3.5.2 修改自定义字段中的模式

自定义字段类型可以使用 __modify_schema__ 类方法自定义为它们生成的模式;有关更多细节,请参见[自定义数据类型](# 3.2.7 自定义数据类型)。

3.5.3 JSON模式类型

类型,自定义字段类型和约束 (像 max_length) 按照以下优先级顺序映射到相应的规范格式 (当有等效的可用格式时):

  1. JSON Schema Core
  2. JSON Schema Validation
  3. OpenAPI Data Types
  4. 标准的 format JSON字段用于为更复杂的 string 子类型定义pydantic扩展。

从Python/pydantic到JSON模式的字段模式映射请参考原始英文文档。

3.5.4 顶层模式生成

您还可以生成一个顶层JSON模式,它的定义中只包含一列模型和相关的子模型:

import json
from pydantic import BaseModel
from pydantic.schema import schema


class Foo(BaseModel):
    a: str = None


class Model(BaseModel):
    b: Foo


class Bar(BaseModel):
    c: int


top_level_schema = schema([Model, Bar], title='My Schema')
print(json.dumps(top_level_schema, indent=2))

输出如下:

{
  "title": "My Schema",
  "definitions": {
    "Foo": {
      "title": "Foo",
      "type": "object",
      "properties": {
        "a": {
          "title": "A",
          "type": "string"
        }
      }
    },
    "Model": {
      "title": "Model",
      "type": "object",
      "properties": {
        "b": {
          "$ref": "#/definitions/Foo"
        }
      },
      "required": [
        "b"
      ]
    },
    "Bar": {
      "title": "Bar",
      "type": "object",
      "properties": {
        "c": {
          "title": "C",
          "type": "integer"
        }
      },
      "required": [
        "c"
      ]
    }
  }
}

3.5.5 模式定制

可以定制生成的 $ref JSON位置:定义总是存储在键 definitions 下,但是可以为引用使用指定的前缀。

如果需要扩展或修改JSON模式默认定义位置,这将非常有用。例如对于 OpenAPI:

import json
from pydantic import BaseModel
from pydantic.schema import schema


class Foo(BaseModel):
    a: int


class Model(BaseModel):
    a: Foo


# Default location for OpenAPI
top_level_schema = schema([Model], ref_prefix='#/components/schemas/')
print(json.dumps(top_level_schema, indent=2))

输出如下:

{
  "definitions": {
    "Foo": {
      "title": "Foo",
      "type": "object",
      "properties": {
        "a": {
          "title": "A",
          "type": "integer"
        }
      },
      "required": [
        "a"
      ]
    },
    "Model": {
      "title": "Model",
      "type": "object",
      "properties": {
        "a": {
          "$ref": "#/components/schemas/Foo"
        }
      },
      "required": [
        "a"
      ]
    }
  }
}

还可以 扩展/覆盖 模型中生成的JSON模式。

为此,使用 Config 子类属性 schema_extra。

例如,你可以在JSON模式中添加 example:

from pydantic import BaseModel


class Person(BaseModel):
    name: str
    age: int

    class Config:
        schema_extra = {
            'examples': [
                {
                    'name': 'John Doe',
                    'age': 25,
                }
            ]
        }


print(Person.schema_json(indent=2))

输出如下:

{
  "title": "Person",
  "type": "object",
  "properties": {
    "name": {
      "title": "Name",
      "type": "string"
    },
    "age": {
      "title": "Age",
      "type": "integer"
    }
  },
  "required": [
    "name",
    "age"
  ],
  "examples": [
    {
      "name": "John Doe",
      "age": 25
    }
  ]
}

对于更细粒度的控制,您可以选择将 schema_extra 设置为可调用对象,并对生成的模式进行后期处理。可调用对象可以有一个或两个位置参数。第一个参数是模式字典。如果有第二个参数,将是模型类。可调用对象期望原地更改模式字典;可调用对象的返回值暂时未被使用。

例如,title 键可以从模型的 properties 中删除:

from typing import Dict, Any, Type
from pydantic import BaseModel


class Person(BaseModel):
    name: str
    age: int

    class Config:
        @staticmethod
        def schema_extra(schema: Dict[str, Any], model: Type['Person']) -> None:
            for prop in schema.get('properties', {}).values():
                prop.pop('title', None)


print(Person.schema_json(indent=2))

输出如下:

{
  "title": "Person",
  "type": "object",
  "properties": {
    "name": {
      "type": "string"
    },
    "age": {
      "type": "integer"
    }
  },
  "required": [
    "name",
    "age"
  ]
}

3.6 导出模型

除了通过名称 (例如 model.foobar ) 直接访问模型属性外,模型还可以通过多种方式转换和导出:

3.6.1 model.dict(…)

这是将模型转换为字典的主要方法。子模型会被递归的转换成字典。

参数:

  • include包含在返回的字典中的字段。参见 [包含和排序的高级用法](# 3.6.6 包含和排序的高级用法)。
  • exclude从返回的字典中排除的字段。参见 [包含和排序的高级用法](# 3.6.6 包含和排序的高级用法)。
  • by_alias字段别名是否应该在返回的字典中作为键;默认为 False。
  • exclude_unset创建模型时未显式设置的字段是否应从返回的字典中排除;默认为 False。在v1.0之前,exclude_unset 被称为 skip_defaults;现在不赞成使用 skip_defaults。
  • exclude_defaults是否应从返回的字典中排除等于其默认值的字段 (无论是否设置);默认为 False。
  • exclude_none是否应从返回的字典中排除等于 None 的字段;默认为 False。

示例:

from pydantic import BaseModel


class BarModel(BaseModel):
    whatever: int


class FooBarModel(BaseModel):
    banana: float
    foo: str
    bar: BarModel


m = FooBarModel(banana=3.14, foo='hello', bar={'whatever': 123})

# returns a dictionary:
print(m.dict())
"""
{
    'banana': 3.14,
    'foo': 'hello',
    'bar': {'whatever': 123},
}
"""
print(m.dict(include={'foo', 'bar'}))
#> {'foo': 'hello', 'bar': {'whatever': 123}}
print(m.dict(exclude={'foo', 'bar'}))
#> {'banana': 3.14}

3.6.2 dict(model) 和迭代

Pydantic 模型也可以使用 dict(model) 转换成字典,并且也可以使用 for field_name, value in model: 迭代模型上的字段。

使用这种方法,将返回原始字段值,因此子模型不会被转换为字典。

示例:

from pydantic import BaseModel


class BarModel(BaseModel):
    whatever: int


class FooBarModel(BaseModel):
    banana: float
    foo: str
    bar: BarModel


m = FooBarModel(banana=3.14, foo='hello', bar={'whatever': 123})

print(dict(m))
"""
{
    'banana': 3.14,
    'foo': 'hello',
    'bar': BarModel(
        whatever=123,
    ),
}
"""
for name, value in m:
    print(f'{name}: {value}')
    #> banana: 3.14
    #> foo: hello
    #> bar: whatever=123

3.6.3 model.copy(…)

copy() 允许复制模型,这对不可变模型尤其有用。

参数:

  • include要包含在返回的字典中的字段。参见 [包含和排序的高级用法](# 3.6.6 包含和排序的高级用法)。
  • exclude要从返回的字典中排序的字典。参见 [包含和排序的高级用法](# 3.6.6 包含和排序的高级用法)。
  • update创建复制的模型时要更改的值的字典。
  • deep是否对新模型进行深复制;默认为 False。

示例:

from pydantic import BaseModel


class BarModel(BaseModel):
    whatever: int


class FooBarModel(BaseModel):
    banana: float
    foo: str
    bar: BarModel


m = FooBarModel(banana=3.14, foo='hello', bar={'whatever': 123})

print(m.copy(include={'foo', 'bar'}))
#> foo='hello' bar=BarModel(whatever=123)
print(m.copy(exclude={'foo', 'bar'}))
#> banana=3.14
print(m.copy(update={'banana': 0}))
#> banana=0 foo='hello' bar=BarModel(whatever=123)
print(id(m.bar), id(m.copy().bar))
#> 140512307789344 140512307789344
# normal copy gives the same object reference for `bar`
print(id(m.bar), id(m.copy(deep=True).bar))
#> 140512307789344 140512307819952
# deep copy gives a new object reference for `bar`

3.6.4 model.json(…)

.json() 方法会将模型序列化为 JSON。通常,.json() 依次调用 .dict() 并序列化其结果。(对于具有自定义根类型的模型,在调用 .dict() 之后,仅序列化 __root__ 键的值)。

参数:

  • include要包含在返回的字典中的字段。参见 [包含和排序的高级用法](# 3.6.6 包含和排序的高级用法)。
  • exclude要从返回的字典中排序的字典。参见 [包含和排序的高级用法](# 3.6.6 包含和排序的高级用法)。
  • by_alias字段别名是否应该在返回的字典中作为键;默认为 False。
  • exclude_unset创建模型时未显式设置的字段是否应从返回的字典中排除;默认为 False。在v1.0之前,exclude_unset 被称为 skip_defaults;现在不赞成使用 skip_defaults。
  • exclude_defaults是否应从返回的字典中排除等于其默认值的字段 (无论是否设置);默认为 False。
  • exclude_none是否应从返回的字典中排除等于 None 的字段;默认为 False。
  • encoder传递给 json.dumps() 的 default 参数的自义编码器函数,默认为设计用于所有常见类型的自定义编码器。
  • **dumps_kwargs传递给 json.dumps() 的其他关键字参数。例如,indent。

Pydantic可以将许多常用的类型序列化为JSON(例如 datetime,date 或 UUID),这通常会失败于一个简单的 json.dumps(foobar)。

from datetime import datetime
from pydantic import BaseModel


class BarModel(BaseModel):
    whatever: int


class FooBarModel(BaseModel):
    foo: datetime
    bar: BarModel


m = FooBarModel(foo=datetime(2032, 6, 1, 12, 13, 14), bar={'whatever': 123})
print(m.json())
#> {"foo": "2032-06-01T12:13:14", "bar": {"whatever": 123}}
3.6.4.1 json_encoders

序列化可以使用 json_encoders 配置属性在模型上定制;键应该是类型,值应该是序列化该类型的函数(见下面的例子):

from datetime import datetime, timedelta
from pydantic import BaseModel
from pydantic.json import timedelta_isoformat


class WithCustomEncoders(BaseModel):
    dt: datetime
    diff: timedelta

    class Config:
        json_encoders = {
            datetime: lambda v: v.timestamp(),
            timedelta: timedelta_isoformat,
        }


m = WithCustomEncoders(dt=datetime(2032, 6, 1), diff=timedelta(hours=100))
print(m.json())
#> {"dt": 1969660800.0, "diff": "P4DT4H0M0.000000S"}

默认情况下,timedelta 被编码为总秒数的简单浮点数。timedelta_isoformat 是一个可选的替代方案,它实现了ISO 8601时间差异编码。

3.6.4.2 序列化子类

注意

在v1.5之前,常见类型的子类不会自动序列化为JSON。

通用类型的子类会像它们的超类一样被自动编码:

from datetime import date, timedelta
from pydantic import BaseModel
from pydantic.validators import int_validator


class DayThisYear(date):
    """
    Contrived example of a special type of date that
    takes an int and interprets it as a day in the current year
    """

    @classmethod
    def __get_validators__(cls):
        yield int_validator
        yield cls.validate

    @classmethod
    def validate(cls, v: int):
        return date.today().replace(month=1, day=1) + timedelta(days=v)


class FooModel(BaseModel):
    date: DayThisYear


m = FooModel(date=300)
print(m.json())
#> {"date": "2020-10-27"}
3.6.4.3 自定义 JSON反序列化

为了提高JSON编码和解码的性能,可以通过配置中的 json_load 和 json_dumps 属性来使用其他JSON实现(例如ujson)。

from datetime import datetime
import ujson
from pydantic import BaseModel


class User(BaseModel):
    id: int
    name = 'John Doe'
    signup_ts: datetime = None

    class Config:
        json_loads = ujson.loads


user = User.parse_raw('{"id": 123,"signup_ts":1234567890,"name":"John Doe"}')
print(user)
#> id=123 signup_ts=datetime.datetime(2009, 2, 13, 23, 31, 30,
#> tzinfo=datetime.timezone.utc) name='John Doe'

ujson 通常不能用于转储JSON,因为它不支持对像 datetime 这样的对象进行编码,也不接受 default 回退函数参数。为此,您可以使用另一个库,如 orjson。

from datetime import datetime
import orjson
from pydantic import BaseModel


def orjson_dumps(v, *, default):
    # orjson.dumps returns bytes, to match standard json.dumps we need to decode
    return orjson.dumps(v, default=default).decode()


class User(BaseModel):
    id: int
    name = 'John Doe'
    signup_ts: datetime = None

    class Config:
        json_loads = orjson.loads
        json_dumps = orjson_dumps


user = User.parse_raw('{"id":123,"signup_ts":1234567890,"name":"John Doe"}')
print(user.json())
#> {"id":123,"signup_ts":"2009-02-13T23:31:30+00:00","name":"John Doe"}

请注意,orjson 本身就考虑了 datetime 编码,这使得它比 json.dumps 更快。但这意味着您不能总是使用 Config.json_encoders 自定义编码。

3.6.5 pickle.dumps(model)

使用与 copy() 相同的管道,pydantic模型支持高效的pickle和unpickle。

import pickle
from pydantic import BaseModel


class FooBarModel(BaseModel):
    a: str
    b: int


m = FooBarModel(a='hello', b=123)
print(m)
#> a='hello' b=123
data = pickle.dumps(m)
print(data)
"""
b'\x80\x04\x95\x8e\x00\x00\x00\x00\x00\x00\x00\x8c\x17exporting_models_pickle
\x94\x8c\x0bFooBarModel\x94\x93\x94)\x81\x94}\x94(\x8c\x08__dict__\x94}\x94(\
x8c\x01a\x94\x8c\x05hello\x94\x8c\x01b\x94K{u\x8c\x0e__fields_set__\x94\x8f\x
94(h\x07h\t\x90\x8c\x1c__private_attribute_values__\x94}\x94ub.'
"""
m2 = pickle.loads(data)
print(m2)
#> a='hello' b=123

3.6.6 包含和排序的高级用法

dict、json 和 copy 方法支持 include 和 exclude 参数,这些参数可以是集合或字典。这允许嵌套选择导出哪些字段:

from pydantic import BaseModel, SecretStr


class User(BaseModel):
    id: int
    username: str
    password: SecretStr


class Transaction(BaseModel):
    id: str
    user: User
    value: int


t = Transaction(
    id='1234567890',
    user=User(
        id=42,
        username='JohnDoe',
        password='hashedpassword'
    ),
    value=9876543210,
)

# using a set:
print(t.dict(exclude={'user', 'value'}))
#> {'id': '1234567890'}

# using a dict:
print(t.dict(exclude={'user': {'username', 'password'}, 'value': ...}))
#> {'id': '1234567890', 'user': {'id': 42}}

print(t.dict(include={'id': ..., 'user': {'id'}}))
#> {'id': '1234567890', 'user': {'id': 42}}

省略号 (...) 表示我们要排除或包括整个键,就像我们将其包括在集合中一样。 当然,可以在任何深度级别进行相同的操作。

从子模型或字典的列表或元组中包括或排除字段时,必须格外小心。 在这种情况下,字典和相关方法期望使用整数键按元素进行逐项包含或排除。 要从列表或元组的每个成员中排除字段,可以按以下方式使用字典键 '__all__':

import datetime
from typing import List

from pydantic import BaseModel, SecretStr


class Country(BaseModel):
    name: str
    phone_code: int


class Address(BaseModel):
    post_code: int
    country: Country


class CardDetails(BaseModel):
    number: SecretStr
    expires: datetime.date


class Hobby(BaseModel):
    name: str
    info: str


class User(BaseModel):
    first_name: str
    second_name: str
    address: Address
    card_details: CardDetails
    hobbies: List[Hobby]


user = User(
    first_name='John',
    second_name='Doe',
    address=Address(
        post_code=123456,
        country=Country(
            name='USA',
            phone_code=1
        )
    ),
    card_details=CardDetails(
        number=4212934504460000,
        expires=datetime.date(2020, 5, 1)
    ),
    hobbies=[
        Hobby(name='Programming', info='Writing code and stuff'),
        Hobby(name='Gaming', info='Hell Yeah!!!'),
    ],
)

exclude_keys = {
    'second_name': ...,
    'address': {'post_code': ..., 'country': {'phone_code'}},
    'card_details': ...,
    # You can exclude fields from specific members of a tuple/list by index:
    'hobbies': {-1: {'info'}},
}

include_keys = {
    'first_name': ...,
    'address': {'country': {'name'}},
    'hobbies': {0: ..., -1: {'name'}},
}

# would be the same as user.dict(exclude=exclude_keys) in this case:
print(user.dict(include=include_keys))
"""
{
    'first_name': 'John',
    'address': {'country': {'name': 'USA'}},
    'hobbies': [
        {
            'name': 'Programming',
            'info': 'Writing code and stuff',
        },
        {'name': 'Gaming'},
    ],
}
"""

# To exclude a field from all members of a nested list or tuple, use "__all__":
print(user.dict(exclude={'hobbies': {'__all__': {'info'}}}))
"""
{
    'first_name': 'John',
    'second_name': 'Doe',
    'address': {
        'post_code': 123456,
        'country': {'name': 'USA', 'phone_code': 1},
    },
    'card_details': {
        'number': SecretStr('**********'),
        'expires': datetime.date(2020, 5, 1),
    },
    'hobbies': [{'name': 'Programming'}, {'name': 'Gaming'}],
}
"""

json 和 copy 方法也是如此。

3.7 Dataclasses

如果您不想使用pydantic的 BaseModel ,您可以在标准 dataclasses (在python 3.7中引入)上获得相同的数据验证。

数据类在Python 3.6中使用 dataclasses的backport包 工作。

from datetime import datetime
from pydantic.dataclasses import dataclass


@dataclass
class User:
    id: int
    name: str = 'John Doe'
    signup_ts: datetime = None


user = User(id='42', signup_ts='2032-06-21T12:00')
print(user)
#> User(id=42, name='John Doe', signup_ts=datetime.datetime(2032, 6, 21, 12, 0))

注意

请记住 pydantic.dataclasses.dataclass 是具有验证的 dataclasses.dataclass 的直接替代。而不是 pydantic.BaseModel (在初始化钩子的工作方式上略有不同) 的替代。在某些情况下,子类化 BaseModel 是更好的选择。

有关更多信息和讨论,请参阅 samuelcolvin/pydantic#710。

您可以使用所有标准的pydantic字段类型,并且结果数据类将与标准库 dataclass 装饰器创建的数据类相同。

可以通过 __pydantic_model__ 访问底层模型及其模式。 另外,可以通过 dataclasses.field 指定需要 default_factory 的字段。

import dataclasses
from typing import List
from pydantic.dataclasses import dataclass


@dataclass
class User:
    id: int
    name: str = 'John Doe'
    friends: List[int] = dataclasses.field(default_factory=lambda: [0])


user = User(id='42')
print(user.__pydantic_model__.schema())
"""
{
    'title': 'User',
    'type': 'object',
    'properties': {
        'id': {'title': 'Id', 'type': 'integer'},
        'name': {
            'title': 'Name',
            'default': 'John Doe',
            'type': 'string',
        },
        'friends': {
            'title': 'Friends',
            'default': [0],
            'type': 'array',
            'items': {'type': 'integer'},
        },
    },
    'required': ['id'],
}
"""

pydantic.dataclasses.dataclass 的参数与标准装饰器相同,除了一个额外的关键字参数 config,它与 Config 的含义相同。

警告

v1.2之后,必须安装 Mypy 插件 来对 Pydantic 数据类进行类型检查。

有关结合验证器与数据类的更多信息,请参见 [dataclass验证器](# 3.3.7 Dataclass 验证器)。

3.7.1 嵌套的数据类

在数据类和普通模型中都支持嵌套数据类。

from pydantic import AnyUrl
from pydantic.dataclasses import dataclass


@dataclass
class NavbarButton:
    href: AnyUrl


@dataclass
class Navbar:
    button: NavbarButton


navbar = Navbar(button=('https://example.com',))
print(navbar)
#> Navbar(button=NavbarButton(href=AnyUrl('https://example.com', scheme='https',
#> host='example.com', tld='com', host_type='domain')))

数据类属性可以由元组、字典或数据类本身的实例填充。

3.7.2 标准库的数据类型和pydantic的数据类型

3.7.2.1 将标准库的数据类型转换成pydantic的数据类型

可以使用 pydantic.dataclasses.dataclass 来装饰标准库的数据类(嵌套或非嵌套),从而使其可以很容易地转换成 pydantic 数据类。

import dataclasses
from datetime import datetime
from typing import Optional

import pydantic


@dataclasses.dataclass
class Meta:
    modified_date: Optional[datetime]
    seen_count: int


@dataclasses.dataclass
class File(Meta):
    filename: str


File = pydantic.dataclasses.dataclass(File)

file = File(
    filename=b'thefilename',
    modified_date='2020-01-01T00:00',
    seen_count='7',
)
print(file)
#> File(modified_date=datetime.datetime(2020, 1, 1, 0, 0), seen_count=7,
#> filename='thefilename')

try:
    File(
        filename=['not', 'a', 'string'],
        modified_date=None,
        seen_count=3,
    )
except pydantic.ValidationError as e:
    print(e)
    """
    1 validation error for File
    filename
      str type expected (type=type_error.str)
    """
3.7.2.2 从标准库的 dataclasses 继承

标准库数据类(嵌套或非嵌套)也可以继承,pydantic将自动验证所有继承的字段。

import dataclasses

import pydantic


@dataclasses.dataclass
class Z:
    z: int


@dataclasses.dataclass
class Y(Z):
    y: int = 0


@pydantic.dataclasses.dataclass
class X(Y):
    x: int = 0


foo = X(x=b'1', y='2', z='3')
print(foo)
#> X(z=3, y=2, x=1)

try:
    X(z='pika')
except pydantic.ValidationError as e:
    print(e)
    """
    1 validation error for X
    z
      value is not a valid integer (type=type_error.integer)
    """
3.7.2.3 将 BaseModel 与标准库的数据类型一起使用

请记住,与 BaseModel 混合使用时,标准库数据类(嵌套或非嵌套)会自动转换为pydantic数据类!此外,生成的pydantic数据类将具有与原始配置完全相同的配置 (order,frozen 等)。

import dataclasses
from datetime import datetime
from typing import Optional

from pydantic import BaseModel, ValidationError


@dataclasses.dataclass(frozen=True)
class User:
    name: str


@dataclasses.dataclass
class File:
    filename: str
    last_modification_time: Optional[datetime] = None


class Foo(BaseModel):
    file: File
    user: Optional[User] = None


file = File(
    filename=['not', 'a', 'string'],
    last_modification_time='2020-01-01T00:00',
)  # nothing is validated as expected
print(file)
#> File(filename=['not', 'a', 'string'],
#> last_modification_time='2020-01-01T00:00')

try:
    Foo(file=file)
except ValidationError as e:
    print(e)
    """
    1 validation error for Foo
    file -> filename
      str type expected (type=type_error.str)
    """

foo = Foo(file=File(filename='myfile'), user=User(name='pika'))
try:
    foo.user.name = 'bulbi'
except dataclasses.FrozenInstanceError as e:
    print(e)
    #> cannot assign to field 'name'
3.7.2.4 使用自定义类型

由于标准库数据类被自动转换为使用自定义类型添加验证,因此可能会导致一些意外行为。在这种情况下,您可以简单地在配置中添加 arbitrary_types_allowed !

import dataclasses

import pydantic


class ArbitraryType:
    def __init__(self, value):
        self.value = value

    def __repr__(self):
        return f'ArbitraryType(value={self.value!r})'


@dataclasses.dataclass
class DC:
    a: ArbitraryType
    b: str


# valid as it is a builtin dataclass without validation
my_dc = DC(a=ArbitraryType(value=3), b='qwe')

try:
    class Model(pydantic.BaseModel):
        dc: DC
        other: str

    Model(dc=my_dc, other='other')
except RuntimeError as e:  # invalid as it is now a pydantic dataclass
    print(e)
    """
    no validator found for <class
    'dataclasses_arbitrary_types_allowed.ArbitraryType'>, see
    `arbitrary_types_allowed` in Config
    """


class Model(pydantic.BaseModel):
    dc: DC
    other: str

    class Config:
        arbitrary_types_allowed = True


m = Model(dc=my_dc, other='other')
print(repr(m))
#> Model(dc=DC(a=ArbitraryType(value=3), b='qwe'), other='other')

3.7.3 初始化钩子

初始化数据类时,可以在 __post_init_post_parse__ 的帮助下在验证后执行代码。 该方法与 __post_init__ 不同,后者在验证之前执行代码。

from pydantic.dataclasses import dataclass


@dataclass
class Birth:
    year: int
    month: int
    day: int


@dataclass
class User:
    birth: Birth

    def __post_init__(self):
        print(self.birth)
        #> {'year': 1995, 'month': 3, 'day': 2}

    def __post_init_post_parse__(self):
        print(self.birth)
        #> Birth(year=1995, month=3, day=2)


user = User(**{'birth': {'year': 1995, 'month': 3, 'day': 2}})
#> {'year': 1995, 'month': 3, 'day': 2}
#> Birth(year=1995, month=3, day=2)

从v1.0版本开始,任何使用 dataclasses.InitVar 注解的字段都将会传递给 __post_init__ 和 __post_init_post_parse__。

from dataclasses import InitVar
from pathlib import Path
from typing import Optional

from pydantic.dataclasses import dataclass


@dataclass
class PathData:
    path: Path
    base_path: InitVar[Optional[Path]]

    def __post_init__(self, base_path):
        print(f'Received path={self.path!r}, base_path={base_path!r}')
        #> Received path='world', base_path='/hello'

    def __post_init_post_parse__(self, base_path):
        if base_path is not None:
            self.path = base_path / self.path


path_data = PathData('world', base_path='/hello')
# Received path='world', base_path='/hello'
assert path_data.path == Path('/hello/world')
3.7.3.1 与标准库数据类的区别

请注意,来自Python 标准库的 dataclasses.dataclass 仅实现 __post_init__ 方法,因为它不运行验证步骤。

当用 pydantic.dataclasses.dataclass 代替 dataclasses.dataclass 的使用时,建议将 __post_init__ 方法中执行的代码移到 __post_init_post_parse__ 方法中,并且只保留需要在验证之前执行的部分代码。

3.7.4 JSON 转储

Pydantic数据类不具有 .json() 函数。 要将它们转储为JSON,您将需要使用 pydantic_encoder,如下所示:

import dataclasses
import json
from typing import List

from pydantic.dataclasses import dataclass
from pydantic.json import pydantic_encoder


@dataclass
class User:
    id: int
    name: str = 'John Doe'
    friends: List[int] = dataclasses.field(default_factory=lambda: [0])


user = User(id='42')
print(json.dumps(user, indent=4, default=pydantic_encoder))
"""
{
    "id": 42,
    "name": "John Doe",
    "friends": [
        0
    ]
}
"""

3.8 验证装饰器

validate_arguments 装饰器允许在调用函数之前使用函数的注解来解析和验证传递给函数的参数。 虽然在后台使用了相同的模型创建和初始化方法,但它提供了一种非常简单的方法,以最少的样板将验证应用于代码。

Beta 测试中

validate_arguments 装饰器仍处于测试中,它是在v1.5中临时添加到pydantic的。在未来的版本中,它可能会有很大的变化,它的接口直到v2才会稳定。在它还处于临时阶段时,来自社区的反馈将非常有用;请在#1205进行评论或者创建一个新问题。

用法示例:

from pydantic import validate_arguments, ValidationError


@validate_arguments
def repeat(s: str, count: int, *, separator: bytes = b'') -> bytes:
    b = s.encode()
    return separator.join(b for _ in range(count))


a = repeat('hello', 3)
print(a)
#> b'hellohellohello'

b = repeat('x', '4', separator=' ')
print(b)
#> b'x x x x'

try:
    c = repeat('hello', 'wrong')
except ValidationError as exc:
    print(exc)
    """
    1 validation error for Repeat
    count
      value is not a valid integer (type=type_error.integer)
    """

3.8.1 参数类型

参数类型是从函数的类型注解进行推断的,没有类型注解的参数被认为是 Any 类型的。因为 validate_arguments 内部使用标准的 BaseModel,因此,[字段类型](# 3.2 字段类型) 中列出的所有类型都可以被验证,包括 pydantic 模型和[自定义数据类型](# 3.2.7 自定义数据类型)。与pydantic的其余部分一样,类型可以在传递给实际函数之前由装饰器强制执行转换:

from pathlib import Path
from typing import Pattern, Optional

from pydantic import validate_arguments, DirectoryPath


@validate_arguments
def find_file(path: DirectoryPath, regex: Pattern, max=None) -> Optional[Path]:
    for i, f in enumerate(path.glob('**/*')):
        if max and i > max:
            return
        if f.is_file() and regex.fullmatch(str(f.relative_to(path))):
            return f


print(find_file('/etc/', '^sys.*'))
#> /etc/sysctl.conf
print(find_file('/etc/', '^foobar.*', max=3))
#> None

需要注意的是,通过将它们作为字符串传递,path 和 regex 分别被装饰器转换为 Path 对象和正则表达式。max 没有类型注释,因此装饰器将其视为 Any 类型。

这样的类型强制转换可能非常有用,但也会造成混乱或者不是我们想要的,请参阅[下面](# 3.8.7.2 强制和严格)关于 validate_arguments 在这方面的限制的讨论。

3.8.2 函数签名

validate_arguments 装饰器被设计为使用所有可能的参数配置和下面这些所有可能的组合来处理函数:

  • 有或者没有默认值的位置参数或关键字参数
  • 通过 * (通常为 *args) 定义的可变位置参数
  • 通过 ** (通常为 **kwargs) 定义的可变关键字参数
  • 仅限关键字参数,即 *, 后面的参数
  • 仅限位置参数,即 , / 前面的参数(Python3.8的新功能)

下面的示例演示了上面所提到的所有参数类型:

from pydantic import validate_arguments


@validate_arguments
def pos_or_kw(a: int, b: int = 2) -> str:
    return f'a={a} b={b}'


print(pos_or_kw(1))
#> a=1 b=2
print(pos_or_kw(a=1))
#> a=1 b=2
print(pos_or_kw(1, 3))
#> a=1 b=3
print(pos_or_kw(a=1, b=3))
#> a=1 b=3


@validate_arguments
def kw_only(*, a: int, b: int = 2) -> str:
    return f'a={a} b={b}'


print(kw_only(a=1))
#> a=1 b=2
print(kw_only(a=1, b=3))
#> a=1 b=3


@validate_arguments
def pos_only(a: int, b: int = 2, /) -> str:  # python 3.8 only
    return f'a={a} b={b}'


print(pos_only(1))
#> a=1 b=2
print(pos_only(1, 2))
#> a=1 b=2


@validate_arguments
def var_args(*args: int) -> str:
    return str(args)


print(var_args(1))
#> (1,)
print(var_args(1, 2))
#> (1, 2)
print(var_args(1, 2, 3))
#> (1, 2, 3)


@validate_arguments
def var_kwargs(**kwargs: int) -> str:
    return str(kwargs)


print(var_kwargs(a=1))
#> {'a': 1}
print(var_kwargs(a=1, b=2))
#> {'a': 1, 'b': 2}


@validate_arguments
def armageddon(
    a: int,
    /,  # python 3.8 only
    b: int,
    c: int = None,
    *d: int,
    e: int,
    f: int = None,
    **g: int,
) -> str:
    return f'a={a} b={b} c={c} d={d} e={e} f={f} g={g}'


print(armageddon(1, 2, e=3))
#> a=1 b=2 c=None d=() e=3 f=None g={}
print(armageddon(1, 2, 3, 4, 5, 6, c=7, e=8, f=9, g=10, spam=11))
#> a=1 b=2 c=7 d=(4, 5, 6) e=8 f=9 g={'spam': 11}

3.8.3 与 mypy 一起使用

validate_arguments 装饰器应该与mypy配合使用,因为它被定义为返回与装饰的函数具有相同签名的函数。 唯一的限制是,由于我们诱使mypy认为装饰器返回的函数与被装饰的函数相同; 要访问[原始函数](# 3.8.4 原始函数)或其他属性,将需要 type: ignore。

3.8.4 原始函数

被装饰的原始函数是可访问的,如果在某些情况下您信任输入参数并希望以最高性能的方式调用该函数,则这是很有用的(请参见下面的[性能说明](# 3.8.7.3 性能)):

from pydantic import validate_arguments


@validate_arguments
def repeat(s: str, count: int, *, separator: bytes = b'') -> bytes:
    b = s.encode()
    return separator.join(b for _ in range(count))


a = repeat('hello', 3)
print(a)
#> b'hellohellohello'

b = repeat.raw_function('good bye', 2, separator=b', ')
print(b)
#> b'good bye, good bye'

3.8.5 异步函数

validate_arguments 装饰器也可以在异步函数上使用:

import asyncio
from pydantic import PositiveInt, ValidationError, validate_arguments


@validate_arguments
async def get_user_email(user_id: PositiveInt):
    # `conn` is some fictional connection to a database
    email = await conn.execute('select email from users where id=$1', user_id)
    if email is None:
        raise RuntimeError('user not found')
    else:
        return email


async def main():
    email = await get_user_email(123)
    print(email)
    #> testing@example.com
    try:
        await get_user_email(-4)
    except ValidationError as exc:
        print(exc.errors())
        """
        [
            {
                'loc': ('user_id',),
                'msg': 'ensure this value is greater than 0',
                'type': 'value_error.number.not_gt',
                'ctx': {'limit_value': 0},
            },
        ]
        """


asyncio.run(main())

3.8.6 自定义配置

可以使用配置设置来自定义 validate_arguments 后面的模型,这等同于在普通模型中设置 Config 子类。

警告

@validate_arguments 尚不支持允许配置别名的 Config 的 fields 和 alias_generator 属性,使用它们会引发错误。

使用装饰器的 config 关键字参数设置配置,该参数可以是配置类,也可以是属性字典,这个属性字典之后会被转换成类。

from pydantic import ValidationError, validate_arguments


class Foobar:
    def __init__(self, v: str):
        self.v = v

    def __add__(self, other: 'Foobar') -> str:
        return f'{self} + {other}'

    def __str__(self) -> str:
        return f'Foobar({self.v})'


@validate_arguments(config=dict(arbitrary_types_allowed=True))
def add_foobars(a: Foobar, b: Foobar):
    return a + b


c = add_foobars(Foobar('a'), Foobar('b'))
print(c)
#> Foobar(a) + Foobar(b)

try:
    add_foobars(1, 2)
except ValidationError as e:
    print(e)
    """
    2 validation errors for AddFoobars
    a
      instance of Foobar expected (type=type_error.arbitrary_type;
    expected_arbitrary_type=Foobar)
    b
      instance of Foobar expected (type=type_error.arbitrary_type;
    expected_arbitrary_type=Foobar)
    """

3.8.7 限制

validate_arguments 是临时发布的,没有添加任何附加功能(稍后可能会添加),请参阅#1205了解更多有关此功能的讨论。

3.8.7.1 验证异常

当前,如果验证失败,则会引发标准的pydantic ValidationError,请参阅[模型错误处理](# 3.1.4 错误处理)。

这很有用,因为它的 str() 方法提供了所发生错误的有用的详细信息,当将错误暴露给最终用户时,.errors() 和 .json() 之类的方法很有用,但是 ValidationError 继承自 ValueError 而不是 TypeError,这可能是意外的,因为Python会在参数无效或缺失时引发TypeError。 将来可以通过允许自定义错误或在默认情况下引发其他异常,或同时解决两者来解决此问题。

3.8.7.2 强制和严格

pydantic目前倾向于尝试强制类型转换,而不是在类型错误的情况下引发异常,请参见[模型数据转换](# 3.1.16 数据转换),并且 validate_arguments 采用了相同的机制。

有关此问题的讨论,请参见#1098 和带有 “strictness” 标签的其他问题。 如果pydantic将来获得 “strict” 模式,validate_arguments 将具有使用此模式的选项,它甚至可能成为装饰器的默认模式。

3.8.7.3 性能

我们已经尽了最大的努力使pydantic尽可能地具有高性能(请参阅[基准测试](# 4. 基准测试),并且仅在定义函数时才执行一次参数检查和模型创建,但是与调用原始函数相比,使用 validate_arguments 装饰器仍然会对性能产生影响。

在许多情况下,这几乎没有或没有明显的影响,但是请注意,validate_arguments 不是强类型语言中的函数定义的等价物或替代项,并且永远不会。

3.8.7.4 返回值

函数的返回值没有根据它的返回类型注解进行验证,这可以在将来作为一个选项添加。

3.8.7.5 配置和验证器

不支持自定义的 Config 上的 fields 和 alias_generator ,参见 [上面](# 3.8.6 自定义配置)。

[验证器](# 3.3 验证器) 也不支持。

3.8.7.6 模型字段和保留参数

以下名称不能由参数使用,因为它们可以在内部用于存储有关函数签名的信息:

  • v__args
  • v__kwargs
  • v__positional_only

这些名称 (以及 args 和 kwargs ) 可能会也可能不会 (取决于函数的签名) 作为字段出现在可通过 .model 访问的内部pydantic模型中。因此,目前这个模型并不是特别有用(例如,对于生成一个模式)。

这在将来应该是可以解决的,因为错误的提出方式被改变了。

3.9 设置管理

pydantic 最有用的功能之一就是配置管理。如果创建一个继承自 BaseSettings 的模型,模型初始化器将尝试从环境变量中读取未作为关键字参数传递的任何字段的值。(如果没有设置匹配的环境变量,仍然使用默认值。)

这使得如下的事情变得很容易:

  • 创建一个明确定义的、具有类型提示的应用程序配置类
  • 自动从环境变量中读取对配置的修改
  • 在需要的地方手动覆盖初始化程序中的特定设置(例如在单元测试中)

例如:

import os
from typing import Set

from pydantic import (
    BaseModel,
    BaseSettings,
    PyObject,
    RedisDsn,
    PostgresDsn,
    Field,
)


class SubModel(BaseModel):
    foo = 'bar'
    apple = 1


class Settings(BaseSettings):
    auth_key: str
    api_key: str = Field(..., env='my_api_key')

    redis_dsn: RedisDsn = 'redis://user:pass@localhost:6379/1'
    pg_dsn: PostgresDsn = 'postgres://user:pass@localhost:5432/foobar'

    special_function: PyObject = 'math.cos'

    # to override domains:
    # export my_prefix_domains='["foo.com", "bar.com"]'
    domains: Set[str] = set()

    # to override more_settings:
    # export my_prefix_more_settings='{"foo": "x", "apple": 1}'
    more_settings: SubModel = SubModel()

    class Config:
        env_prefix = 'my_prefix_'  # defaults to no prefix, i.e. ""
        fields = {
            'auth_key': {
                'env': 'my_auth_key',
            },
            'redis_dsn': {
                'env': ['service_redis_dsn', 'redis_url']
            }
        }

os.environ['my_api_key'] = 'abc123'
os.environ['my_auth_key'] = '123abc'
print(Settings().dict())
"""
{
    'auth_key': '123abc',
    'api_key': 'abc123',
    'redis_dsn': RedisDsn('redis://user:pass@localhost:6379/1', scheme='redis', user='user', password='pass', host='localhost', host_type='int_domain', port='6379', path='/1'),
    'pg_dsn': PostgresDsn('postgres://user:pass@localhost:5432/foobar', scheme='postgres', user='user', password='pass', host='localhost', host_type='int_domain', port='5432', path='/foobar'),
    'special_function': <built-in function cos>,
    'domains': set(),
    'more_settings': {'foo': 'bar', 'apple': 1},
}
"""

3.9.1 环境变量名称

以下规则用于确定为给定字段读取哪些环境变量:

  • 默认情况下,环境变量名是通过连接前缀和字段名构建的:
  • 例如,要覆盖上面的 special_function,可以使用:
export my_prefix_special_function='foo.bar'
  • 注意 1:默认前缀是空字符串。
  • 注意 2:构建变量名称时字段别名将被忽略。
  • 自定义环境变量名称可以使用两种方法设置:
  • Config.fields['field_name']['env'] (参见上面的 auth_key 和 redis_dsn )
  • Field(..., env=...) (参见上面的 api_key )
  • 当指定了自定义环境变量名称时,可以提供一个字符串或者一个字符串列表。
  • 当指定一个字符串列表时,顺序很重要:第一个发现的值将被使用。
  • 例如,对于上面的 redis_dns,service_redis_dns 比 redis_url 的优先级要高。

警告

由于在查找环境变量来填充设置模型时,v1.0 pydantic不考虑字段别名,所以按照上面的描述使用 env 代替。

为了帮助从别名过渡到 env,当在没有自定义 env 变量名称的设置模型上使用别名时,将发出警告。如果您真的打算使用别名,那么可以忽略警告,或者设置 env 来禁止它。

大小写敏感可以通过 Config 开启:

from pydantic import BaseSettings


class Settings(BaseSettings):
    redis_host = 'localhost'

    class Config:
        case_sensitive = True

当 case_sensitive 为 True 时,环境变量名必须匹配字段名(可选带有前缀),因此在本例中只能通过 export redis_host 修改redis_host。如果希望将环境变量命名为全大写,那么也应该将属性命名为全大写。您仍然可以通过 Field(..., env=...) 随意命名环境变量。

注意

在Windows上,Python的 os 模块总是将环境变量视为大小写不敏感的,因此 case_sensitive 配置设置将不起作用——设置将总是被更新为忽略大小写。

3.9.2 解析环境变量

对于大多数简单的字段类型(比如 int、float、str 等),环境变量值的解析方式与直接传递给初始化器(作为字符串)的方式相同。

复杂类型,如 List、Set、Dict 和子模型,通过将环境变量的值作为JSON编码的字符串来填充。

3.9.3 Dotenv (.env)支持

注意

dotenv文件解析需要安装 python-dotenv。这可以通过 pip install python-dotenv 或 pip install pydantic[dotenv] 来完成。

Dotenv文件 (通常命名为 .env ) 是一种常见的模式,它可以方便地以独立于平台的方式使用环境变量。

dotenv文件遵循与所有环境变量相同的一般原则,看起来如下:

# ignore comment
ENVIRONMENT="production"
REDIS_ADDRESS=localhost:6379
MEANING_OF_LIFE=42
MY_VAR='Hello world'

在 .env 文件中填充变量后,pydantic支持通过两种方式加载该文件:

  1. 在 BaseSetting 类中的 Config 上设置 env_file (和 env_file_encoding ,如果你不想使用 OS 的默认编码的话)。
class Settings(BaseSettings):
    ...

    class Config:
        env_file = '.env'
        env_file_encoding = 'utf-8'
  1. 使用 _env_file 关键字参数 (和 _env_file_encoding,如果需要) 实例化 BaseSettings 派生类:
settings = Settings(_env_file='prod.env', _env_file_encoding='utf-8')

无论哪种情况,传递的参数的值都可以是任何有效路径或文件名,可以是绝对路径或相对于当前工作目录的相对路径。pydantic将从那里通过加载变量并对其进行验证来为您处理所有事情。

即使使用dotenv文件,pydantic仍将读取环境变量以及dotenv文件,环境变量将始终优先于从dotenv文件加载的值。

在实例化时(方法2)通过 _env_file 关键字参数传递文件路径将覆盖 Config 类上设置的值(如果有)。如果以上代码段结合使用,则将加载 prod.env,而将忽略 .env。

还可以通过将 None 作为实例化关键字参数传递给 BaseSettings 的派生类的初始化方法来使用关键字参数覆盖来告诉Pydantic根本不加载任何文件 (即使在 Config 类中设置了一个文件),例如 settings = Settings(_env_file = None)。

由于使用 python-dotenv 来解析文件,因此可以使用类似于bash的语义(例如 export) (取决于操作系统和环境) 来允许dotenv文件也与 source 一起使用,请参阅 python-dotenv的文档 以了解更多详细信息。

3.9.4 安全支持

在文件中放置机密值是一种常见的模式,可以为应用程序提供敏感的配置。

安全文件遵循与dotenv文件相同的主体,只是它只包含一个值,并且文件名用作键。 机密文件如下所示:

/var/run/database_password:

super_secret_database_password

一旦有了安全文件,可以使用两种方式来加载:

  1. 在 BaseSettings 类的 Config 中将 secrets_dir 设置为安全文件所在的目录:
class Settings(BaseSettings):
    ...
    database_password: str

    class Config:
        secrets_dir = '/var/run'
  1. 使用 _secret_dir 关键字参数实例化 BaseSettings 的派生类:
settings = Settings(_secrets_dir='/var/run')

无论哪种情况,传递的参数值可以是任意有效的目录,或者到当前工作目录的相对或绝对路径。pydantic将从那里通过加载变量并验证它们来为您处理一切。

即使使用安全目录,pydantic 仍然会从 dotenv 文件或环境读取环境变量,dotenv文件和环境变量总是优先于从安全目录中读取的值。

在初始化时通过 _secrets_dir 关键字参数传递的文件路径将会覆盖在 Config 上设置的值(如果有的话)。

3.9.4.1 用例:Docker Secrets

Docker Secrets可用于为在Docker容器中运行的应用程序提供敏感的配置。 要在一个pytantic应用程序中使用这些机密,过程很简单。 有关在Docker中创建、管理和使用机密的更多信息,请参阅Docker官方文档。

首先,定义 Settings 类:

class Settings(BaseSettings):
    my_secret_data: str

    class Config:
        secrets_dir = '/run/secrets'

注意

默认情况下,Docker使用 /run/secrets 作为目标挂载点。如果你想使用一个不同的位置,改变相应地 Config.secrets_dir。

然后,通过 Docker 命令行接口创建机密:

printf "This is a secret" | docker secret create my_secret_data -

最后,运行 Docker 容器内的应用程序并提供新创建的机密:

docker service create --name pydantic-with-secrets --secret my_secret_data pydantic-app:latest

3.9.5 字段值优先级

如果同一个设置字段有多种方式指定一个值,则选择的值按以下优先级降序确定:

  1. 传递给 Settings 类的初始化器的参数。
  2. 环境变量,例如上面描述的 my_prefix_special_function 。
  3. 从 dotenv( .env 文件) 加载的变量。
  4. 从机密目录加载的变量。
  5. Settings 模型的默认字段值。

3.10 延迟注解

注意

通过 future 导入和 ForwardRef 的延迟注解都需要python 3.7以上。

延迟注解 (如 PEP563 所述) “just work”。

from __future__ import annotations
from typing import List
from pydantic import BaseModel


class Model(BaseModel):
    a: List[int]


print(Model(a=('1', 2, 3)))
#> a=[1, 2, 3]

在内部,pydantic将调用类似于 typing.get_type_hints 的方法来解析注解。

在引用类型尚未定义的情况下,可以使用 ForwardRef (尽管在自引用模型的情况下直接引用类型或通过它的字符串引用是一个更简单的解决方案)。

在某些情况下,一个 ForwardRef 将不能在模型创建期间被解析。例如,当模型将自身引用为字段类型时,就会发生这种情况。当这种情况发生时,您需要在模型创建后调用 update_forward_refs,然后才能使用它:

from typing import ForwardRef
from pydantic import BaseModel

Foo = ForwardRef('Foo')


class Foo(BaseModel):
    a: int = 123
    b: Foo = None


Foo.update_forward_refs()

print(Foo())
#> a=123 b=None
print(Foo(b={'a': '321'}))
#> a=123 b=Foo(a=321, b=None)

警告

为了将字符串(类型名称)解析为注解(类型),pydantic需要一个名称空间字典来执行查找。 为此,它使用 module.__dict__,就像 get_type_hints 一样。 这意味着pydantic可能无法与模块的全局范围中未定义的类型配合使用。

例如,下面的实例可以正常运行:

from __future__ import annotations
from typing import List  # <-- List is defined in the module's global scope
from pydantic import BaseModel


def this_works():
    class Model(BaseModel):
        a: List[int]

    print(Model(a=(1, 2)))

而下面的则不行:

from __future__ import annotations
from pydantic import BaseModel


def this_is_broken():
    # List is defined inside the function so is not in the module's
    # global scope!
    from typing import List

    class Model(BaseModel):
        a: List[int]

    print(Model(a=(1, 2)))

解决这个问题超出了pydantic的要求:要么删除 future 的导入,要么全局声明类型。

3.10.1 自引用模型

如果创建模型后调用了 update_forward_refs() 函数,则还支持具有自引用模型的数据结构 (果您忘记了,将会提示一条友好的错误消息)。

在模型中,可以使用字符串引用尚未构建的模型:

from pydantic import BaseModel


class Foo(BaseModel):
    a: int = 123
    #: The sibling of `Foo` is referenced by string
    sibling: 'Foo' = None


Foo.update_forward_refs()

print(Foo())
#> a=123 sibling=None
print(Foo(sibling={'a': '321'}))
#> a=123 sibling=Foo(a=321, sibling=None)

从python 3.7开始,您还可以按其类型引用它,前提是您导入了 annotations (请参见上文,具体取决于Python和pydantic版本)。

from __future__ import annotations
from pydantic import BaseModel


class Foo(BaseModel):
    a: int = 123
    #: The sibling of `Foo` is referenced directly by type
    sibling: Foo = None


Foo.update_forward_refs()

print(Foo())
#> a=123 sibling=None
print(Foo(sibling={'a': '321'}))
#> a=123 sibling=Foo(a=321, sibling=None)

3.11 与 mypy 一起使用

pydantic模型与mypy一起工作,只要你使用仅注解版本的必需字段:

from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, NoneStr


class Model(BaseModel):
    age: int
    first_name = 'John'
    last_name: NoneStr = None
    signup_ts: Optional[datetime] = None
    list_of_ints: List[int]


m = Model(age=42, list_of_ints=[1, '2', b'3'])
print(m.middle_name)  # not a model field!
Model()  # will raise a validation error for age and list_of_ints

可以通过下面的方式使用 mypy 运行你的代码:

mypy \
  --ignore-missing-imports \
  --follow-imports=skip \
  --strict-optional \
  pydantic_mypy_test.py

如果你在上面的示例代码上调用 mypy,将会看到 mypy 发现了属性访问错误:

13: error: "Model" has no attribute "middle_name"

3.11.1 严格模式

为了让您的代码在使用 --strict-optional 选项时能够通过,您需要为所有字段使用 Optional[] 或 Optional[] 的别名,并将字段默认值设置为 None。(这是mypy的标准。)

Pydantic提供了一些有用的可选或联合类型:

  • NoneStr,即 Optional[str]
  • NoneBytes,即 Optional[bytes]
  • StrBytes,即 Union[str, bytes]
  • NoneStrBytes,即 Optional[StrBytes]

这些还不够,你当然可以自己定义。

3.12 与 devtools 一起使用

注意

python-devtools 由 Pydantic 的主要开发者开发。

python-devtools( pip install devtools )提供了许多在python开发期间有用的工具,包括 debug() 。这是 print() 的一个替代品,它能提供更易阅读的格式化输出,并显式打印语句位于哪个文件的哪个行上,以及打印了什么值。

pydantic通过在大多数公共类上实现 __pretty__ 方法与 devtools 集成。

特别是在检查模型时,debug() 很有用:

from datetime import datetime
from typing import List
from pydantic import BaseModel

from devtools import debug


class Address(BaseModel):
    street: str
    country: str
    lat: float
    lng: float


class User(BaseModel):
    id: int
    name: str
    signup_ts: datetime
    friends: List[int]
    address: Address


user = User(
    id='123',
    name='John Doe',
    signup_ts='2019-06-01 12:22',
    friends=[1234, 4567, 7890],
    address=dict(street='Testing', country='uk', lat=51.5, lng=0),
)
debug(user)
print('\nshould be much easier read than:\n')
print('user:', user)

将会在终端打印如下输出:

docs/examples/devtools_main.py:31 <module>
    user: User(
        id=123,
        name='John Doe',
        signup_ts=datetime.datetime(2019, 6, 1, 12, 22),
        friends=[
            1234,
            4567,
            7890,
        ],
        address=Address(
            street='Testing',
            country='uk',
            lat=51.5,
            lng=0.0,
        ),
    ) (User)

should be much easier read than:

user: id=123 name='John Doe' signup_ts=datetime.datetime(2019, 6, 1, 12, 22) friends=[1234, 4567, 7890] address=Address(street='Testing', country='uk', lat=51.5, lng=0.0)

4. 基准测试

以下是将pydantic与其他验证库进行比较的原始基准测试结果。

pydantic

1.1

43.6μs

attrs + cattrs

19.3.0

1.4x slower

59.9μs

valideer

0.4.2

1.4x slower

61.8μs

marshmallow

3.2.2

2.5x slower

107.6μs

trafaret

2.0.0

3.4x slower

148.7μs

django-rest-framework

3.10.3

12.6x slower

551.2μs

cerberus

1.3.2

26.3x slower

1146.3μs

有关测试用例的更多详细信息,请参见基准代码。 随意建议更多软件包以进行基准测试或改进现有软件包。

基准测试是使用Python 3.7.4运行的,上面列出的软件包版本是通过pypi在Ubuntu 18.04上安装的。

5. Mypy 插件

由于个人不使用 mypy,故略过。

详情请参考英文文档。

6. Pycharm 插件

尽管pydantic可以与任何现成的IDE很好地兼容,但JetBrains插件存储库中的PyCharm可以使用提供改进的pydantic集成的PyCharm插件。 您可以从插件市场免费安装插件(PyCharm的Preferences->Plugin->Marketplace->搜索 “pydantic”)。

该插件当前支持以下功能:

  • 对 pydantic.BaseModel.__init__ 进行:
  • 审查
  • 自动补全
  • 类型检查
  • 对 pydantic.BaseModel 的字段进行:
  • 重构重命名字段会更新 __init__ 调用,并影响子类和超类; 重构重命名 __init__ 关键字参数会更新字段名称,并影响子类和超类;

可以在官方插件页面和Github存储库中找到更多信息。

7. 代码生成

datamodel-code-generator 项目是一个库和命令行实用程序,可从几乎任何数据源生成pydantic模型,包括:

  • OpenAPI 3 (YAML/JSON)
  • JSON 模式
  • JSON/YAML 数据 (将会转变为 JSON 模式)

当您发现自己有任何JSON可转换数据但没有pydantic模型时,此工具将允许您根据需要生成类型安全的模型层次结构。

7.1 安装

pip install datamodel-code-generator

7.2 示例

在这种情况下,datamodel-code-generator 从一个 JSON 模式文件创建 pydantic 模型:

Person.json:

{
  "$id": "person.json",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Person",
  "type": "object",
  "properties": {
    "first_name": {
      "type": "string",
      "description": "The person's first name."
    },
    "last_name": {
      "type": "string",
      "description": "The person's last name."
    },
    "age": {
      "description": "Age in years.",
      "type": "integer",
      "minimum": 0
    },
    "pets": {
      "type": "array",
      "items": [
        {
          "$ref": "#/definitions/Pet"
        }
      ]
    },
    "comment": {
      "type": "null"
    }
  },
  "required": [
      "first_name",
      "last_name"
  ],
  "definitions": {
    "Pet": {
      "properties": {
        "name": {
          "type": "string"
        },
        "age": {
          "type": "integer"
        }
      }
    }
  }
}

model.py:

# generated by datamodel-codegen:
#   filename:  person.json
#   timestamp: 2020-05-19T15:07:31+00:00
from __future__ import annotations
from typing import Any, List, Optional
from pydantic import BaseModel, Field, conint


class Pet(BaseModel):
    name: Optional[str] = None
    age: Optional[int] = None


class Person(BaseModel):
    first_name: str = Field(..., description="The person's first name.")
    last_name: str = Field(..., description="The person's last name.")
    age: Optional[conint(ge=0)] = Field(None, description='Age in years.')
    pets: Optional[List[Pet]] = None
    comment: Optional[Any] = None

更多信息,请参考 官方文档。