Pydantic官方文档

1. 简介

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

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

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

1.1 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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',
}
"""

这里发生了什么:

  • idint类型;注释声明告诉pydantic该字段是必须的。

    如果可能,字符串、字节或浮点数将强制转换为int,否则将引发异常。

  • name 从默认值推断为其为str类型,该字段不是必须的,因为它有默认值。

  • signup_tsdatetime 类型,该字段不是必须的,默认值为 None

    pydantic会将表示unix时间戳(例如1496498400)的 int 类型或表示时间和日期的字符串处理成datetime 类型。

  • friends 使用Python的typing 系统,需要一个整数列表,就像id 字段一样,类整数的对象将会被转换为整数。

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

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

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[
{
"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 类允许在 “验证此请求数据” 上下文和 “加载我的系统设置” 上下文中使用。主要区别在于,系统设置可以从环境变量读取,并且通常需要更复杂的对象,如DSN和Python对象。

  • 快速

    pydantic比其他所有测试库都要快。

  • 可以验证复杂结构

    使用递归pydantic模型、typing 的标准类型 (如 ListTupleDict等) 和验证器,可以很清晰且容易地定义、验证和解析复杂数据模式。

  • 可拓展

    pydantic允许定义自定义数据类型,或者您可以使用被 validator 装饰器装饰的模型上的方法来扩展验证。

  • dataclasses 集成

    和 BaseModel 一样,pydantic提供了一个 dataclass 装饰器,它创建带有输入数据解析和验证的(几乎)普通的Python数据类。

2. 安装

1
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 是否已经编译,可以使用如下方法:

1
2
import pydantic
print('compiled:', pydantic.compiled)

Pydantic 有三个可选依赖:

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

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

1
2
3
4
5
pip install pydantic[email]
# or
pip install pydantic[dotenv]
# or just
pip install pydantic[email,dotenv]

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

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

1
2
3
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,dotenv]

3. 用法详解

3.1 模型

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

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

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

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

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

3.1.1 基本模型的使用

1
2
3
4
5
from pydantic import BaseModel

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

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

1
user = User(id='123')

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

1
assert user.id == 123

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

1
assert user.name == 'Jane Doe'

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

1
assert user.__fields_set__ == {'id'}

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

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

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

1
2
user.id = 321
assert user.id == 321

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

3.1.1.1 模型属性

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

  • dict()

    返回模型的字段和值的字典。参见 导出模型)。

  • json()

    返回表示dict()的 JSON 字符串。参见 导出模型)。

  • copy()

    返回模型的副本(默认情况下为浅副本)。参见 导出模型)。

  • parse_obj()

    如果一个对象不是字典,可以使用该方法将其加载到具有错误处理的模型中。参见 帮助函数

  • parse_raw()

    用于加载多种格式字符串的实用程序。参见 帮助函数

  • parse_file()

    parse_raw()类似,但是作用于文件路径。参见 帮助函数

  • from_orm()

    从任意类加载数据到模型中。参见 ORM 模式

  • schema()

    返回一个将模型表示为 JSON 模式的字典。参见 模式

  • schema_json()

    返回表示schema()的 JSON 字符串。参见 模式

  • construct()

    用于创建模型而不执行验证的类方法;参见 创建未经验证的模型

  • __fields_set__

    当模型实例初始化时设置的字段名称集合。

  • __config__

    模型的配置类。参见 模型配置

3.1.2 递归模型

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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.1.3 ORM 模式

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

要达到这个目的,需要:

  • 模型的内部类 Configorm_mode 属性必须设置为 True

  • 必须使用特殊的构造函数 from_orm 来创建模型实例。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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 0x7efde1c2ed30>
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字段命名列。在这种情况下,可以使用字段别名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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'}}

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

3.1.3.2 递归 ORM 模型

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 类处理任意类,该类试图为任何类提供一个类似于字典的接口。可以通过将 GetterDict 的自定义子类设置为 Config.getter_dict的值来覆盖默认行为(参考 模型配置)。

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

3.1.4 错误处理

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

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

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

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

  • e.errors()

    返回输入数据中发现的错误的列表。

  • e.json()

    返回一个表示 errors 的 JSON。

  • str(e)

    返回人类可读的错误表示。

每一个错误对象包含:

  • loc

    错误的位置列表。列表中的第一项将是发生错误的字段,如果该字段是子模型,则将出现后续项以指示错误的嵌套位置。

  • type

    计算机可读的错误类型的标识符。

  • msg

    人类可读的错误解释。

  • ctx

    包含呈现错误消息所需的值的可选对象。

下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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"
}
]
"""
知识就是财富
如果您觉得文章对您有帮助, 欢迎请我喝杯水!