Pydantic 是一个使用Python类型提示来进行数据验证和设置管理的库。Pydantic定义数据应该如何使用纯Python规范用并进行验证。PEP 484 从Python3.5开始引入了类型提示的功能,PEP 526 使用Python3.6中的变量注释语法对其进行了拓展。Pydantic使用这些注释来验证不受信任的数据是否采用了您想要的形式。
示例:
from datetime import datetime
from typing import List
from pydantic import BaseModel
class User(BaseModel):
id: int
name = 'John Doe'
signup_ts: datetime = None
friends: List[int] = []
external_data = {'id': '123', 'signup_ts': '2017-06-01 12:22', 'friends': [1, '2', b'3']}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
这里发生了什么:
id
是 int 类型;注释声明告诉pydantic该字段是必须的。如果可能,字符串、字节或浮点数将强制转换为int,否则将引发异常。name
从默认值推断为其为 str 类型,该字段不是必须的,因为它有默认值。signup_ts
是 datetime 类型,该字段不是必须的,默认值为 None。pydantic会将表示unix时间戳(例如1496498400)的 int 类型或表示时间和日期的字符串处理成 datetime 类型。如果验证失败,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": "type_error.datetime"
},
{
"loc": [
"friends",
2
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
"""
pydantic使用了一些很酷的新语言特性,但我为什么要使用它呢?
不需要太高的学习成本
不需要学习新的模式定义微语言。如果您了解Python(也许略读了 类型提示文档),您就知道如何使用pydantic。
与你的IDE/linter/brain配合得很好
因为pydantic数据结构只是您定义的类的实例;自动完成、linting、mypy和您的直觉都应该能够正确地处理经过验证的数据。
多用途
pydantic的 BaseSettions 类允许在 验证此请求数据 上下文和 加载我的系统设置 上下文中使用它。主要区别在于,系统设置可以由环境变量更改默认值,而且通常需要更复杂的对象,如DSNs和Python对象。
快速
pydantic比其他所有测试库都要快。
可以验证复杂结构
使用递归pydantic模型、typing 模块的 List 和 Dict 等,并且验证器可以清晰、轻松地定义复杂的数据模式,然后进行检查。
可拓展
pydantic允许定义自定义数据类型,或者您可以使用使用validator装饰器装饰的模型上的方法来扩展验证器。
pip install pydantic
Pydantic除了Python3.6或Python3.7(和Python3.6中的 dataclasses 包)之外,不需要其他依赖项。
如果想让pydantic更加快速的解析JSON,你可以添加 ujson 作为可选的依赖项。类似的,pydantic 的email验证依赖于 email-validator :
pip install pydantic[ujson]
# or
pip install pydantic[email]
# or just
pip install pydantic[ujson,email]
当然,你也可以使用 pip install … 手动安装这些依赖项。
pydantic使用 typing 类型定义更复杂的对象:
from typing import Dict, 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
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
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_tuple=(1, 2, 3, 4)).simple_tuple) # > (1, 2, 3, 4)
print(Model(tuple_of_different_types=[1, 2, 3, 4]).tuple_of_different_types) # > (1, 2.0, '3', True)
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_set={1, 2, 3, 4}).simple_set)
print(Model(set_bytes={b'1', b'2', b'3', b'4'}).set_bytes)
print(Model(str_or_bytes='hello world').str_or_bytes)
print(Model(str_or_bytes=b'hello world').str_or_bytes)
print(Model(none_or_str='hello world').none_or_str)
print(Model(none_or_str=None).none_or_str)
print(Model(sequence_of_ints=[1, 2, 3, 4]).sequence_of_ints) # > [1, 2, 3, 4]
print(Model(compound={'name': [{1, 2, 3}], b'entitlement': [{10, 5}]}).compound)
注意:v0.14 版本的新功能。
如果不希望使用pydantic的 BaseModel,则可以在标准 dataclasses 上获得相同的数据验证(在Python 3.7中引入)。
dataclasses 在Python3.6中使用 dataclasses backport package 来工作。
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字段类型,得到的数据类将与使用标准库的 dataclass
装饰器创建的数据类相同。
pydantic.dataclasses.dataclass 的参数与标准装饰器相同,除了一个与 Config 具有相同含义的额外关键字参数 config。
注意:作为pydantic的 dataclasses 能够很好地处理 mypy的副作用, 配置参数将在IDE和mypy中显示为不可用。使用
@dataclass(..., config=Config) # type: ignore
作为解决方案。查看 python/mypy#6239 来了解为什么要这样。
从 v0.17 版本开始,dataclasses 和正常的模型都支持嵌套的 dataclasses。
from pydantic import UrlStr
from pydantic.dataclasses import dataclass
@dataclass
class NavbarButton:
href: UrlStr
@dataclass
class Navbar:
button: NavbarButton
navbar = Navbar(button=('https://example.com',))
print(navbar)
# > Navbar(button=NavbarButton(href='https://example.com'))
dataclasses 属性可以由元组、字典或该 dataclass 的示例来填充。
Pydantic使用Python的标准 enum
类定义选择:
from enum import Enum, IntEnum
from pydantic import BaseModel
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())
# > CookingModel fruit= tool=
print(CookingModel(tool=2, fruit='banana'))
# > CookingModel fruit= tool=
print(CookingModel(fruit='other'))
# will raise a validation error
自定义验证和对象之间的复杂关系可以使用 validator
装饰器来获得。
validator
装饰器通过装饰类中的方法来验证字段,被装饰的方法必须是 类方法。
validator
装饰器有几个可选的参数:
*
以解包的方式指定所有的字段,这意味着将为所有字段调用验证器。>>> from pydantic import BaseModel, ValidationError, validator
>>> class UserModel(BaseModel):
... name: 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
...
>>> UserModel(name='samuel colvin', password1='zxcvbn', password2='zxcvbn')
<UserModel name='Samuel Colvin' password1='zxcvbn' password2='zxcvbn'>
>>> try:
... UserModel(name='samuel', password1='zxcvbn', password2='zxcvbn2')
... except ValidationError as e:
... print(e)
...
2 validation errors
name
must contain a space (type=value_error)
password2
passwords do not match (type=value_error)
需要注意的几件事情:
UserModel
而不是 UserModel
的实例。(cls, value)
或 (cls, value, values, config, field)
。从 v0.20开始,任何 (values, config, field)
的子集都是允许的。例如 (cls, value, field)
,但是,由于检查 validator 的方式,可变的关键字参数(**kwargs
)必须叫做 kwargs
。if 'password1' in values and …
语句。注意:从v0.18开始,磨人不会对字典的键调用验证器。如果希望验证键,请使用 whole 。
validator 可以做更复杂的事情:
>>> import json
>>> from typing import List
>>> from pydantic import BaseModel, ValidationError, validator
>>> class DemoModel(BaseModel):
... numbers: List[int] = []
... people: List[str] = []
... @validator('people', 'numbers', pre=True, whole=True)
... def json_decode(cls, v):
... if isinstance(v, str):
... try:
... return json.loads(v)
... except ValueError:
... pass
... return v
... @validator('numbers')
... def check_numbers_low(cls, v):
... if v > 4:
... raise ValueError(f'number too large {v} > 4')
... return v
... @validator('numbers', whole=True)
... def check_sum_numbers_low(cls, v):
... if sum(v) > 8:
... raise ValueError(f'sum of numbers greater than 8')
... return v
...
>>> DemoModel(numbers='[1, 2, 1, 3]')
<DemoModel numbers=[1, 2, 1, 3] people=[]>
>>> try:
... DemoModel(numbers='[1, 2, 5]')
... except ValidationError as e:
... print(e)
...
1 validation error
numbers -> 2
number too large 5 > 4 (type=value_error)
>>> try:
... DemoModel(numbers=[3, 3, 3])
... except ValidationError as e:
... print(e)
...
1 validation error
numbers
sum of numbers greater than 8 (type=value_error)
出于性能原因,默认情况下,对于不提供值的字段不调用验证器。然而,在某些情况下,总是调用验证器是有用的或必需的,例如设置动态默认值。
>>> 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()
...
>>> DemoModel()
<DemoModel ts=datetime.datetime(2019, 4, 26, 8, 26, 13, 74477)>
>>> DemoModel(ts='2017-11-08T14:00')
<DemoModel ts=datetime.datetime(2017, 11, 8, 14, 0)>
验证器在 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()
...
>>> DemoDataclass()
DemoDataclass(ts=None)
>>> DemoDataclass(ts='2017-11-08T14:00')
DemoDataclass(ts=datetime.datetime(2017, 11, 8, 14, 0))
在类创建时,将检查验证器,以确认它们指定的字段实际上存在于模型中。
但是,有时并不需要这样做:当定义一个验证器来验证继承模型上的字段时。在这种情况下,您应该在验证器上设置 check_fields=False
。
更复杂的层次数据结构可以使用模型作为注解中的类型来定义。
...
只表示与上面的注解声明相同的 Required。
>>> 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] = ...
...
>>> s = Spam(foo={'count': 4}, bars=[{'apple': 'x1'}, {'apple': 'x2'}])
>>> s
<Spam foo=<Foo count=4 size=None> bars=[<Bar apple='x1' banana='y'>, <Bar apple='x2' banana='y'>]>
>>> s.dict()
{'foo': {'count': 4, 'size': None}, 'bars': [{'apple': 'x1', 'banana': 'y'}, {'apple': 'x2', 'banana': 'y'}]}
Pydantic会自动从模型创建JSON Schema。
>>> from enum import Enum
>>> from pydantic import BaseModel, Schema
>>> 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 = Schema(...)
... gender: Gender = Schema(
... None,
... alias='Gender',
... )
... snap: int = Schema(
... 42,
... title='The Snap',
... description='this is the value of snap',
... gt=30,
... lt=50,
... )
... class Config:
... title = 'Main'
...
>>> print(MainModel.schema())
{'title': 'Main', 'description': 'This is the description of the main model', 'type': 'object', 'properties': {'foo_bar': {'$ref': '#/definitions/FooBar'}, 'Gender': {'title': 'Gender', 'enum': ['male', 'female', 'other', 'not_given'], 'type': 'string'}, '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']}}}
>>> print(MainModel.schema_json(indent=4))
输出:
{
"title": "Main",
"description": "This is the description of the main model",
"type": "object",
"properties": {
"foo_bar": {
"$ref": "#/definitions/FooBar"
},
"Gender": {
"title": "Gender",
"enum": [
"male",
"female",
"other",
"not_given"
],
"type": "string"
},
"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"
]
}
}
}
生成的模式符合以下规范: JSON Schema Core,JSON Schema Validation and OpenAPI。
BaseModel.schema
会返回一个表示JSON Schema的字典对象。
BaseModel.schema_json
会返回一个表示表示JSON Schema的字符串。
使用的子模型被添加到JSON Schema中,并根据规范进行引用。所有子模型(及其子模型)模式都会被直接放在JSON Schema的顶级键值对中,便于重用和引用。所有通过 Schema
类进行修改的 子模型 ,比如自定义标题、描述和默认值,都会被递归地包含,而不是引用。模型的描述从类的文档字符串或 Schema
类的参数描述中获得。
Schema类以可选的方式提供关于字段和验证、参数的额外信息:
Schema
类会替换字段的默认值,它的第一个参数用来设置默认值。使用 ...
表示这个字段是必须的。field_name.title()
。exclusiveMinimum
注解。minimum
注解。exclusiveMaximum
注解。maximum
注解。multipleOf
注解。minLength
注释。maxLength
注释。pattern
注解。**
:任何其他关键字参数(例如 example
)将会被逐个添加到字段的模式。Config
类的 fields
特性代替 Schema
类用以设置以上参数中除过 default
参数的其他参数的值。
默认情况下,模式是使用别名作为键生成的,也可以使用模型属性名而不是使用别名生成:
MainModel.schema/schema_json(by_alias=False)
当有一个等价物可用时,类型、自定义字段类型和约束(如 max_length)映射到相应的 JSON Schema Core 规范格式,接下来, JSON Schema Validation, OpenAPI Data Types(基于JSON模式)等将以标准的JSON Schema验证关键字 format
为更复杂的 string
类型定义Pydantic子类型扩展。
要查看从Python/Pydantic 到 JSON Schema的字段模式映射,请参考 字段模式映射。
您还可以生成一个顶级JSON模式,该JSON模式的 definitions
中只包含一个模型列表及其所有相关子模块:
>>> 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=4))
{
"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"
]
}
}
}
您可以自定义生成的 $ref
,$ref
的值的仍然在键定义中指定,您仍然可以从键定义中获取键值,但是引用将指向你定义的前缀,而不是默认的前缀。
扩展或修改JSON模式的默认定义位置非常有用,例如使用OpenAPI:
>>> import json
>>> from pydantic import BaseModel
>>> from pydantic.schema import schema
>>> class Foo(BaseModel):
... a: int
...
>>> class Model(BaseModel):
... a: Foo
...
>>> top_level_schema = schema([Model], ref_prefix='#/components/schemas/') # Default location for OpenAPI
>>> print(json.dumps(top_level_schema, indent=4))
{
"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"
]
}
}
}
当Pydantic在它正在验证的数据中发现错误时,就会引发 ValidationError 异常。
注意:验证代码不应该引发 ValidationError 异常,而应该引发将会被捕获并用于填充 ValidationError异常的 ValueError 或 TypeError (或其子类)异常。。
无论发现多少错误,都只会引发一个异常,ValidationError 将包含关于所有错误及其发生方式的信息。
你可以通过下面几种方式访问这些错误:
e.errors
的JSON。e.errors
以人类可读的字符串返回。每一个 error
对象包含以下属性:
下面的例子展示了错误处理的过程:
>>> 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
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"
}
]
如果你自定义了数据类型或者验证器,你应该使用 TypeError 和 ValueError 引发错误:
>>> 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"
}
}
]
Pydantic支持以下的datetime类型:
YYYY-MM-DD[T]HH:MM[:SS[.ffffff]][Z[±]HH[:]MM]]]
YYYY-MM-DD
[-][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'
... )
>>>
>>> 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)}
Pydantic附带了许多用于解析或验证公共对象的实用工具。
import uuid
from decimal import Decimal
from ipaddress import IPv4Address, IPv6Address, IPv4Interface, IPv6Interface, IPv4Network, IPv6Network
from pathlib import Path
from uuid import UUID
from pydantic import (DSN, UUID1, UUID3, UUID4, UUID5, BaseModel, DirectoryPath, EmailStr, FilePath, NameEmail,
NegativeFloat, NegativeInt, PositiveFloat, PositiveInt, PyObject, UrlStr, conbytes, condecimal,
confloat, conint, constr, IPvAnyAddress, IPvAnyInterface, IPvAnyNetwork, SecretStr, SecretBytes)
class Model(BaseModel):
cos_function: PyObject = None
path_to_something: Path = None
path_to_file: FilePath = None
path_to_directory: DirectoryPath = None
short_bytes: conbytes(min_length=2, max_length=10) = None
strip_bytes: conbytes(strip_whitespace=True)
short_str: constr(min_length=2, max_length=10) = None
regex_str: constr(regex='apple (pie|tart|sandwich)') = None
strip_str: constr(strip_whitespace=True)
big_int: conint(gt=1000, lt=1024) = None
mod_int: conint(multiple_of=5) = None
pos_int: PositiveInt = None
neg_int: NegativeInt = None
big_float: confloat(gt=1000, lt=1024) = None
unit_interval: confloat(ge=0, le=1) = None
mod_float: confloat(multiple_of=0.5) = None
pos_float: PositiveFloat = None
neg_float: NegativeFloat = None
email_address: EmailStr = None
email_and_name: NameEmail = None
url: UrlStr = None
password: SecretStr = None
password_bytes: SecretBytes = None
db_name = 'foobar'
db_user = 'postgres'
db_password: str = None
db_host = 'localhost'
db_port = '5432'
db_driver = 'postgres'
db_query: dict = None
dsn: DSN = None
decimal: Decimal = None
decimal_positive: condecimal(gt=0) = None
decimal_negative: condecimal(lt=0) = None
decimal_max_digits_and_places: condecimal(max_digits=2, decimal_places=2) = None
mod_decimal: condecimal(multiple_of=Decimal('0.25')) = None
uuid_any: UUID = None
uuid_v1: UUID1 = None
uuid_v3: UUID3 = None
uuid_v4: UUID4 = None
uuid_v5: UUID5 = None
ipvany: IPvAnyAddress = None
ipv4: IPv4Address = None
ipv6: IPv6Address = None
ip_vany_network: IPvAnyNetwork = None
ip_v4_network: IPv4Network = None
ip_v6_network: IPv6Network = None
ip_vany_interface: IPvAnyInterface = None
ip_v4_interface: IPv4Interface = None
ip_v6_interface: IPv6Interface = None
m = Model(
cos_function='math.cos',
path_to_something='/home',
path_to_file='/home/file.py',
path_to_directory='home/projects',
short_bytes=b'foo',
strip_bytes=b' bar',
short_str='foo',
regex_str='apple pie',
strip_str=' bar',
big_int=1001,
mod_int=155,
pos_int=1,
neg_int=-1,
big_float=1002.1,
mod_float=1.5,
pos_float=2.2,
neg_float=-2.3,
unit_interval=0.5,
email_address='Samuel Colvin ',
email_and_name='Samuel Colvin ',
url='http://example.com',
password='password',
password_bytes=b'password2',
decimal=Decimal('42.24'),
decimal_positive=Decimal('21.12'),
decimal_negative=Decimal('-21.12'),
decimal_max_digits_and_places=Decimal('0.99'),
mod_decimal=Decimal('2.75'),
uuid_any=uuid.uuid4(),
uuid_v1=uuid.uuid1(),
uuid_v3=uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org'),
uuid_v4=uuid.uuid4(),
uuid_v5=uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org'),
ipvany=IPv4Address('192.168.0.1'),
ipv4=IPv4Address('255.255.255.255'),
ipv6=IPv6Address('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'),
ip_vany_network=IPv4Network('192.168.0.0/24'),
ip_v4_network=IPv4Network('192.168.0.0/24'),
ip_v6_network=IPv6Network('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128'),
ip_vany_interface=IPv4Interface('192.168.0.0/24'),
ip_v4_interface=IPv4Interface('192.168.0.0/24'),
ip_v6_interface=IPv6Interface('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128')
)
print(m.dict())
"""
{
'cos_function': ,
'path_to_something': PosixPath('/home'),
'path_to_file': PosixPath('/home/file.py'),
'path_to_directory': PosixPath('/home/projects'),
'short_bytes': b'foo',
'strip_bytes': b'bar',
'short_str': 'foo',
'regex_str': 'apple pie',
'strip_str': 'bar',
'big_int': 1001,
'mod_int': 155,
'pos_int': 1,
'neg_int': -1,
'big_float': 1002.1,
'mod_float': 1.5,
'pos_float': 2.2,
'neg_float': -2.3,
'unit_interval': 0.5,
'email_address': '[email protected]',
'email_and_name': ")>,
'url': 'http://example.com',
'password': SecretStr('**********'),
'password_bytes': SecretBytes(b'**********'),
...
'dsn': 'postgres://postgres@localhost:5432/foobar',
'decimal': Decimal('42.24'),
'decimal_positive': Decimal('21.12'),
'decimal_negative': Decimal('-21.12'),
'decimal_max_digits_and_places': Decimal('0.99'),
'mod_decimal': Decimal('2.75'),
'uuid_any': UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8'),
'uuid_v1': UUID('c96e505c-4c62-11e8-a27c-dca90496b483'),
'uuid_v3': UUID('6fa459ea-ee8a-3ca4-894e-db77e160355e'),
'uuid_v4': UUID('22209f7a-aad1-491c-bb83-ea19b906d210'),
'uuid_v5': UUID('886313e1-3b8a-5372-9b90-0c9aee199e5d'),
'ipvany': IPv4Address('192.168.0.1'),
'ipv4': IPv4Address('255.255.255.255'),
'ipv6': IPv6Address('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'),
'ip_vany_network': IPv4Network('192.168.0.0/24'),
'ip_v4_network': IPv4Network('192.168.0.0/24'),
'ip_v6_network': IPv4Network('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128'),
'ip_vany_interface': IPv4Interface('192.168.0.0/24'),
'ip_v4_interface': IPv4Interface('192.168.0.0/24'),
'ip_v6_interface': IPv6Interface('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128')
}
"""
字段也可以是 Callable
类型:
from typing import Callable
from pydantic import BaseModel
class Foo(BaseModel):
callback: Callable[[int], int]
m = Foo(callback=lambda x: x)
print(m)
# Foo callback= at 0x7f16bc73e1e0>
警告:
Callable
字段只执行参数是否可调用的简单检查,不执行参数、参数的类型或返回类型的验证。
可以使用 SecretStr
和 SecretBytes
数据类型来存储您不希望在日志记录或回溯中可见的敏感信息。SecretStr
和 SecretBytes
将在转换为JSON时被格式化为 **********
或空。
>>> from typing import List
>>> from pydantic import BaseModel, SecretStr, SecretBytes, ValidationError
>>> class SimpleModel(BaseModel):
... password: SecretStr
... password_bytes: SecretBytes
...
>>> sm = SimpleModel(password='IAmSensitive', password_bytes=b'IAmSensitiveBytes')
>>> print(sm)
SimpleModel password=SecretStr('**********') password_bytes=SecretBytes(b'**********')
>>> 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
password
str type expected (type=type_error.str)
password_bytes
byte type expected (type=type_error.bytes)
可以使用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]]
...
>>> SimpleJsonModel(json_obj='{"b": 1}')
<SimpleJsonModel json_obj={'b': 1}>
>>> ComplexJsonModel(json_obj='[1, 2, 3]')
<ComplexJsonModel json_obj=[1, 2, 3]>
>>> try:
... ComplexJsonModel(json_obj=12)
... except ValidationError as e:
... print(e)
...
1 validation error
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
json_obj
Invalid JSON (type=value_error.json)
>>> try:
... ComplexJsonModel(json_obj='["a", "b"]')
... except ValidationError as e:
... print(e)
...
2 validation errors
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)
你也可以定义你自己的数据类型。类方法 __get_validators__
将会被调用,用以获取验证器来解析和验证数据。
注意:从v0.17 开始,
__get_validators__
变更成get_validators
,原来的名称仍然可以使用,但是在将来的版本中可能会被移除。
>>> from pydantic import BaseModel, ValidationError
>>> class StrictStr(str):
... @classmethod
... def __get_validators__(cls):
... yield cls.validate
... @classmethod
... def validate(cls, v):
... if not isinstance(v, str):
... raise ValueError(f'strict string: str expected not {type(v)}')
... return v
...
>>> class Model(BaseModel):
... s: StrictStr
...
>>> Model(s='hello world')
<Model s='hello world'>
>>> try:
... print(Model(s=123))
... except ValidationError as e:
... print(e.json())
...
[
{
"loc": [
"s"
],
"msg": "strict string: str expected not " ,
"type": "value_error"
}
]
Pydantic在模型中提供了3个帮助函数来解析数据,这三个帮助函数都是类方法。
__init__
方法相同,除过当传递的对象不是 dict
,将会引发 ValidationError(而不是Python引发 TypeError)。parse_obj
方法。传递的数据的类型通过 content_type
参数来推断,否则假定为JSON。parse_raw
方法,如果未指定 content_type
参数的值,将会由文件拓展名来推断文件内容所表示的Python对象的类型。>>> import pickle
>>> from datetime import datetime
>>> from pydantic import BaseModel, ValidationError
>>> class User(BaseModel):
... id: int
... name = 'John Doe'
... signup_ts: datetime = None
...
>>> m = User.parse_obj({'id': 123, 'name': 'bob'})
>>> m
<User id=123 name='bob' signup_ts=None>
>>> try:
... User.parse_obj(['not', 'a', 'dict'])
... except ValidationError as e:
... print(e)
...
1 validation error
__obj__
User expected dict not list (type=type_error)
>>> m = User.parse_raw('{"id": 123, "name": "James"}')
>>> m
<User id=123 name='James' signup_ts=None>
>>> 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)
>>> m
<User id=123 name='James' signup_ts=datetime.datetime(2017, 7, 14, 0, 0)>
注意:pickle 允许对复杂对象进行编码,要使用它,需要显式地将解析函数的 allow_pickle 参数的值设置为 True。
Pydantic的行为可以通过模型中的 Config
类来控制。
Config
类包含如下一些类属性:
2 ** 16
Extra
的枚举值。默认值是 Extra.ignore
。value
特性填充模型,而不是使用原始枚举。这在您希望序列化 model.dict()
时非常有用。默认值为 False。schema
类。默人在 None。警告:在启用 allow_population_by_alias 之前请三思!启用它可能会导致先前正确的代码变得不正确。例如,假设您有一个名为 card_number 的字段和别名 cardNumber。在 allow_population_by_alias 属性值为 False 时,只使用键 card_number 尝试解析对象将会失败。但是,如果 allow_population_by_alias 属性值设置为 True,那么现在可以从 cardNumber 或 card_number 填充 card_number 字段,并且先前无效的示例对象现在将是有效的。对于某些用例,这可能是需要的,但是在其他用例中(比如这里给出的例子),放松对别名的严格限制可能会引入bug。
>>> 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
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
name
max_length:10 (type=value_error.any_str.max_length; limit_value=10)
Pydantic最有用的应用之一是定义默认设置,并允许它们被环境变量或关键字参数覆盖(例如在单元测试中)。
>>> from typing import Set
>>> from pydantic import BaseModel, DSN, BaseSettings, PyObject
>>> class SubModel(BaseModel):
... foo = 'bar'
... apple = 1
...
>>> class Settings(BaseSettings):
... redis_host = 'localhost'
... redis_port = 6379
... redis_database = 0
... redis_password: str = None
... auth_key: str = ...
... invoicing_cls: PyObject = 'path.to.Invoice'
... db_name = 'foobar'
... db_user = 'postgres'
... db_password: str = None
... db_host = 'localhost'
... db_port = '5432'
... db_driver = 'postgres'
... db_query: dict = None
... dsn: DSN = None
... # 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 'APP_'
... fields = {
... 'auth_key': {
... 'alias': 'my_api_key'
... }
... }
...
这里的 redis_port
可以通过 export MY_PREFIX_REDIS_PORT=6380
修改,auth_key
可以通过 exportmy_api_key=6380
修改。
默认情况下,BaseSettings
按照以下优先级来获取字段值(其中3的优先级最高,并会重写其他两项):
Settings
中设置的值。MY_PREFIX_REDIS_PORT
。Settings
类时传递的参数。可以通过重写 BaseSettings
类的 _build_values
方法来更改这个默认行为。
复杂的类型,如 list
,dict
,set
和子模型可以通过JSON环境变量来设置。
可以以不区分大小写的方式读取环境变量:
from pydantic import BaseSettings
class Settings(BaseSettings):
redis_host = 'localhost'
class Config:
case_insensitive = True
这里的 redis_port
可以通过 export APP_REDIS_HOST
, export app_redis_host
或 export app_REDIS_host
修改。
在某些情况下,模型的形状直到运行时才知道,由于这个原因,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
这里,DynamicFoobarModel
和 StaticFoobarModel
是完全相同的。
字段要么由表单的一个元组 (
定义,要么仅由一个默认值定义。特殊的关键字参数 __config__
和 __base__
可以用来定制新模型。这包括使用额外的字段扩展基本模型。
>>> from pydantic import BaseModel, create_model
>>> class FooModel(BaseModel):
... foo: str
... bar: int = 120
...
>>> BarModel = create_model('BarModel', apple='russet', banana='yellow', __base__=FooModel)
>>> BarModel
<class 'BarModel'>
>>> ', '.join(BarModel.__fields__.keys())
'foo, bar, apple, banana'
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'])
>>> m.age
42
>>> try:
... Model()
... except ValidationError as e:
... print(e)
...
2 validation errors
age
field required (type=value_error.missing)
也可以通过mypy以如下方式运行:
mypy --ignore-missing-imports --follow-imports=skip --strict-optional pydantic_mypy_test.py
要给代码传递 --strict-optional
选项,需要对所有没有缺省值的字段使用 Optional[]
或 Optional[]
的别名,这是mypy的标准做法。
Pydantic提供了一些有用的可选或联合类型:
NoneStr
又叫做 Optional[str]
NoneBytes
又叫做 Optional[bytes]
StrBytes
又叫做 Union[str, bytes]
NoneStrBytes
又叫做 Optional[StrBytes]
如果这些还不够,你可以定义。
省略号符号 …
不能与mypy一同工作。您需要像上面的示例那样使用注释字段。
警告:请注意,仅使用注释字段将更改元数据中字段的顺序和错误:始终以仅有注释的字段优先,但仍然按照字段定义的顺序。
要解决这个问题,可以使用 Required
(通过 pydantic import Required
)字段作为仅使用省略号或注释的字段的别名。
可以通过 allow_mutation = False
将模型配置为不可变的,这将防止更改模型的属性。
警告: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
>>> foobar.a
'hello'
>>> foobar.b
{'apple': 'pear'}
>>> foobar.b['apple'] = 'grape'
>>> foobar.b
{'apple': 'grape'}
试图更改 a
导致了一个错误,并且它仍然保持不变,但是字典对象 b
是可变的,foobar
的不变性不会阻止 b
的值的更改。
dict
方法返回一个包含模型属性的字典。子模型被递归地转换为字典, copy
方法允许模型被复制,这对于不可变模型特别有用。
dict
、copy
和 json
方法(如下所述)都使用可选的 include
和 exclude
关键字参数来控制返回或复制哪些属性。copy
方法接受额外的关键字参数 update
,它接受将属性映射到新值的类型为字典对象的值,这些新值将在复制模型并进行深度复制时应用。
dict
和 json
采用可选的 skip_defaults
关键字参数,该参数将跳过未显式设置的属性。这对于减少具有许多不经常更改的默认字段的模型的序列化大小非常有用。
>>> 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': 456})
>>> m.dict()
{'banana': 3.14, 'foo': 'hello', 'bar': {'whatever': 456}}
>>> m.dict(include={'foo', 'bar'})
{'foo': 'hello', 'bar': {'whatever': 456}}
>>> m.dict(exclude={'foo', 'bar'})
{'banana': 3.14}
>>> m.copy()
<FooBarModel banana=3.14 foo='hello' bar=<BarModel whatever=456>>
>>> m.copy(include={'foo', 'bar'})
<FooBarModel foo='hello' bar=<BarModel whatever=456>>
>>> m.copy(exclude={'foo', 'bar'})
<FooBarModel banana=3.14>
>>> m.copy(update={'banana': 0})
<FooBarModel banana=0 foo='hello' bar=<BarModel whatever=456>>
>>> id(m.bar), id(m.copy().bar)
(4417630280, 4417630280)
>>> id(m.bar), id(m.copy(deep=True).bar)
(4417630280, 4417630928)
Pydantic支持将数据序列化为JSON和Pickle,当然可以通过处理 dict()
方法的结果将数据序列化为您喜欢的任何其他格式。
json()
方法将模型序列化为JSON,然后 json()
方法调用 dict()
方法并序列化其结果。
可以通过在模型上使用 json_encoders
配置属性定制序列化,键应该是类型,值应该是序列化该类型的函数,参见下面的示例。
如果这还不够,json()
方法接受一个可选的 encoder
参数,该参数允许完全控制如何将非标准类型编码为JSON。
>>> from datetime import datetime, timedelta
... from pydantic import BaseModel
... from pydantic.json import timedelta_isoformat
>>> class BarModel(BaseModel):
... whatever: int
...
>>> class FooBarModel(BaseModel):
... foo: datetime
... bar: BarModel
...
>>> m = FooBarModel(foo=datetime(2032, 6, 1, 12, 13, 14), bar={'whatever': 400})
>>> m.json()
'{"foo": "2032-06-01T12:13:14", "bar": {"whatever": 400}}'
>>> class WithCustomEncoders(BaseModel):
... dt: datetime
... diff: timedelta
... class Config:
... json_encoders = {
... datetime: lambda v: (v - datetime(1970, 1, 1)).total_seconds(),
... timedelta: timedelta_isoformat
... }
...
>>> m = WithCustomEncoders(dt=datetime(2032, 6, 1), diff=timedelta(hours=100))
>>> m.json()
'{"dt": 1969660800.0, "diff": "P4DT4H0M0.000000S"}'
默认情况下,时间增量被编码为一个简单的浮点数,以总秒为单位。timedelta_isoformat
作为一个可选选项提供,它实现了ISO 8601时间差异编码。
使用与 copy()
相同的管道,Pydantic支持有效的 pickle 和 unpick。
>>> import pickle
>>> from pydantic import BaseModel
>>> class FooBarModel(BaseModel):
... a: str
... b: int
...
>>> m = FooBarModel(a='hello', b=100)
>>> m
<FooBarModel a='hello' b=100>
>>> data = pickle.dumps(m)
>>> data
b'\x80\x03c__main__\nFooBarModel\nq\x00)\x81q\x01}q\x02(X\n\x00\x00\x00__values__q\x03}q\x04(X\x01\x00\x00\x00aq\x05X\x05\x00\x00\x00helloq\x06X\x01\x00\x00\x00bq\x07KduX\x0e\x00\x00\x00__fields_set__q\x08cbuiltins\nset\nq\t]q\n(h\x07h\x05e\x85q\x0bRq\x0cub.'
>>> m2 = pickle.loads(data)
>>> m2
<FooBarModel a='hello' b=100>
Pydantic模型可以与Python的抽象基类(ABC)一起使用。
>>> import abc
>>> from pydantic import BaseModel
>>> class FooBarModel(BaseModel):
... a: str
... b: int
... @abc.abstractmethod
... def my_abstract_method(self):
... pass
...
注意:通过
future
导入和ForwardRef
的延迟注解都需要Python 3.7+。
全局延迟注解在Pydantic中应该 只是能工作。
>>> from __future__ import annotations
... from typing import List
... from pydantic import BaseModel
>>> class Model(BaseModel):
... a: List[int]
...
>>> Model(a=('1', 2, 3))
<Model a=[1, 2, 3]>
在内部,Pydantic将调用类似于 typing.get_type_hints
的方法用于解析注释。
要使用 ForwardRef
,您可能需要在创建模型之后调用 model .update_forward_refs()
,这是因为在下面的示例中,Foo
在创建之前并不存在(显然),所以 ForwardRef
不能首先被解析。您必须等到 Foo
创建之后,然后调用 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()
>>> Foo()
<Foo a=123 b=None>
>>> Foo(b={'a': '321'})
<Foo a=123 b=<Foo a=321 b=None>>
警告:要将字符串(类型名称)解析成注解(类型),pydantic需要查找
moduel.__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(): from typing import List # <-- List is defined inside the function so is not in the module's global scope class Model(BaseModel): a: List[int] print(Model(a=(1, 2)))
解决这个问题超出了pydantic的调用:要么删除
future
的导入,要么全局地声明类型。