Pydantic
Теги: pydantic pip data-bases python
Типизация для python Документация. Использует [type-annotation] на базе [mypy]
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',
}
"""
Установка
pip install pydantic
Три варианта
pip install pydantic[email]
валидация имейлов
pip install pydantic[dotenv]
.env support - [[.env-переменные-окружения]]
pip install pydantic[email,dotenv]
Использование
Модели
from pydantic import BaseModel
class User(BaseModel):
id: int
name = 'Jane Doe'
Каждый класс модели наследует базовой модели pydantic. В данном случае тип name инферится из типа строки. Это поле не обязательно, так как установлено дефолтное значение. Тогда создавая инстанс класса, мы получим аттрибуты:
user = User(id='123')
assert user.id == 123
assert user.name == 'Jane Doe'
# name не создается при инициализации, т.к. имеет дефолтное значение
assert user.__fields_set__ == {'id'}
# поля полученные, при инициализации
assert user.dict() == dict(user) == {'id': 123, 'name': 'Jane Doe'}
# наконец, у нас есть полный доступ к атрибутам
user.id = 321
assert user.id == 321
dict()
словарь полей и значений моделиjson()
тоже вамое в виде jsoncopy()
копия модели (по дефолту shallow)parse_obj()
метод для парсинга и лоада объекта в модель с выдачей ошибки, если объект не похож на fictparse_raw()
тоже самое из jsonparse_file()
из файлаfrom_orm()
из класса ОРМschema()
возвращает json-схему в виде словаряschema_json()
в виде jsonconstruct()
создание модели без валидации (когда данные из проверенногно источника - на в 30 раз быстрее)_fields_set__
имена полей в виде множества__fields__
словарь полей__config__
конфиг
Можно выстраивать модели иерархически
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'},
],
}
"""
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 0x7fb266c7ef40>
co_model = CompanyModel.from_orm(co_orm)
print(co_model)
#> id=123 public_key='foobar' name='Testing' domains=['example.com',
#> 'foobar.com']
Иногда нужно дать название колонке, после того, как зарезервирвоано название поля.
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'}}
ORM модели могут быть рекурсивными
Доступ к ошибкам
Осуществляется через ValidationError. Поднимается один эксепшен вне зависимости от кол-ва ошибок, с информацией по которому можно работать через несколько методов. Кроме того, можно реализовать кастомные ошибки.
Поддерживаются дополнительные методы создания моделей (см. в models)
Например можно сделать даныне модели неизменяемыми. Можно создавать дженерик модели для последующего использования в качестве “шаблона”. Можно использовать абстрактные базовы классы, устанавливать строгий порядок полей (поля должны оставаться в том порядке, в котором они были заданы в модели)
Обязательные поля
from pydantic import BaseModel, Field
class Model(BaseModel):
a: int
b: int = ...
c: int = Field(...)
вариант рекуаред поля с опциональным значением
class Model(BaseModel):
a: Optional[int]
b: Optional[int] = ...
c: Optional[int] = Field(...)
Поля с динамическим required значением
С помощью default_factory
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)
Автоматическая конфертация
Пайдантик автоматически конвертирует некотоыре типы данных, что может привести к потере информации
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'}
Типы полей
Поддерживает все стандартные типы #python
Типы:
None
ortype(None)
orLiteral[None]
bool
int
float
str
bytes
list
(принимает list, tuple, set, frozenset, deque и генераторы)tuple
(принимает list, tuple, set, frozenset, deque и генераторы)dict
set
(принимает list, tuple, set, frozenset, deque и генераторы)frozenset
(принимает list, tuple, set, frozenset, deque и генераторы)deque
(принимает list, tuple, set, frozenset, deque и генераторы)datetime.date
datetime.time
datetime.datetime
datetime.timedelta
typing.Any
любое значениеtyping.Annotated
анотированное значениеtyping.TypeVar
константа, базирующаяся на этомtyping.Union
несколько разных типовtyping.Optional
обертка надUnion[x, None]
typing.List
typing.Tuple
- subclass of
typing.NamedTuple
- subclass of
collections.namedtuple
typing.Dict
и subclasstyping.Set
typing.FrozenSet
typing.Deque
typing.Sequence
typing.Iterable
зарезевировано под другие итераторыtyping.Type
typing.Callable
typing.Pattern
для regexipaddress.IPv4Address
и другие ip…enum.Enum
и subclass ofenum.Enum
enum.IntEnum
и subclassdecimal.Decimal
конвертит в строку, затем в decimalpathlib.Path
uuid.UUID
ByteSize
Пример с итераторами
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
DateTime типы
- datetime fields can be:
- datetime, existing datetime object
- int or float, assumed as Unix time, i.e. seconds (if >= -2e10 or <= 2e10) or milliseconds (if < -2e10or > 2e10) since 1 January 1970
- str, following formats work:
- YYYY-MM-DD[T]HH:MM[:SS[.ffffff]][Z or [±]HH[:]MM]]]
- int or float as a string (assumed as Unix time)
- date fields can be:
- date, existing date object
- int or float, see datetime
- str, following formats work:
- YYYY-MM-DD
- int or float, see datetime
- time fields can be:
- time, existing time object
- str, following formats work:
- HH:MM[:SS[.ffffff]][Z or [±]HH[:]MM]]]
- timedelta fields can be:
- timedelta, existing timedelta object
- int or float, assumed as seconds
- str, following formats work:
- [-][DD ][HH:MM]SS[.ffffff]
- [±]P[DD]DT[HH]H[MM]M[SS]S (ISO 8601 format for timedelta)
Boolean
Будет ошибка валидации, если значение не одно из;
- True or False
- 0 or 1
- a str which when converted to lower case is one of ‘0’, ‘off’, ‘f’, ‘false’, ‘n’, ‘no’, ‘1’, ‘on’, ‘t’, ‘true’, ‘y’, ‘yes’
- a bytes which is valid (per the previous rule) when decoded to str
Calable
Позволяет передавать функцию и указывать, какой выход в ней ожидается. Валидация не проверяте типы аргументов функции, только то, что этот объект вызываемый.
Type
Когда мы должны проверить, что объект является производным от tyoe
, т.е. классом, не инстансом.
Literal Type
Появился в #python 3.8 typing.Literal
(или typing_extensions.Literal
для 3.8). позволяет определить только специфичные значения литералов. Позволяет проверять одно или больше специфичных значений без использования валидаторов.
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))
Пример с Union
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
Анотированные типы
Пример с NamedTuple
from typing import NamedTuple
from pydantic import BaseModel, ValidationError
class Point(NamedTuple):
x: int
y: int
class Model(BaseModel):
p: Point
print(Model(p=('1', '2')))
#> p=Point(x=1, y=2)
pydantic types
FilePath
DirectoryPath
EmailStr
NameEmail
PyObject
Color
html/css цвет вот в таком формате. Поддерживается несколько методов конвертации.Json
ПримерPaymentCardNumber
ПримерAnyUrl
описание про урлыAnyHttpUrl
HttpUrl
PostgresDsn
RedisDsn
stricturl
UUID1
и 2, 3, 4, 5SecretBytes
и т.д. если нужно скрыт часть инфы из логированияIPvAnyAddress
и т.д.NegativeFloat
и т.д.- несколько методов, которые принимают методы, содержащие другие типы
Constrained types
Ограничение типов по форматам, диапазонам и т.д. через приставку con*
from decimal import Decimal
from pydantic import (
BaseModel,
NegativeFloat,
NegativeInt,
PositiveFloat,
PositiveInt,
NonNegativeFloat,
NonNegativeInt,
NonPositiveFloat,
NonPositiveInt,
conbytes,
condecimal,
confloat,
conint,
conlist,
conset,
constr,
Field,
)
class Model(BaseModel):
lower_bytes: conbytes(to_lower=True)
short_bytes: conbytes(min_length=2, max_length=10)
strip_bytes: conbytes(strip_whitespace=True)
lower_str: constr(to_lower=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
non_neg_int: NonNegativeInt
non_pos_int: NonPositiveInt
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
non_neg_float: NonNegativeFloat
non_pos_float: NonPositiveFloat
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)
Описание всех аргументов (методов) смотри там же
Есть еще несколько специфичных случаев и можно задавать свои типы.
Validators
Использование классметода для валидации данных в модели
Это позволяет возвращать определенные данные, после валидации
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)
"""
В разделе примеры как использовать кастом валидацию.
Config
class Sprints(BaseModel):
user: User
sprints: List[Sprint]
class Config:
orm_mode = True
title
заголовко json схемыanystr_strip_whitespace
и т.д.validate_all
extra
забыть, применить или игнорировать экстра атрибуты при инциализацииallow_mutation
для неизменяемых типовfrozen
открывает дорогу к хешированию инстансов модели- …
orm_mode
использовать как модель для ORMalias_generator
schema_extra
json_loads
кастомные ф-ии для jsonjson_dumps
json_encoders
Сконфигурировать можно глобально, вот так:
class BaseModel(PydanticBaseModel):
class Config:
arbitrary_types_allowed = True
Alias
Пример алиас-генератора, чтобы перегнать все змеиные имена в верблюжьи
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'}
SCHEMA
Возвращается json-схема, в т.ч. можно в [openapi-specification]
Смотри статью про данные, которые попадают в схему, анотированные типы в схеме, валидацию схемы и кастомизацию:
Экспорт моделей в другие форматы данных
Dataclasses
Использование validate_arguments
Находится в бете с версии 1.5. Пример использования:
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)
"""
Аргументы для валидации инфирятся из аннотации типов функции. Если тип не анотирован, он инферится как any
Settings managements
Если создать модель и унаследовать ее от BaseSettings
, эта модель позволит определить значения любых полей, которые не определены ключевым аргументом, из переменных окружения. Это позволяет сделать следующее:
- сделать понятный класс конфигураций для приложения
- автоматически чиать конфигурации из переменных окружения
- в ручную переписывать специфические настройки, к примеру для тестов
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']
}
}
print(Settings().dict())
"""
{
'auth_key': 'xxx',
'api_key': 'xxx',
'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},
}
"""
Есть .env поддержка
.env файл
# ignore comment
ENVIRONMENT="production"
REDIS_ADDRESS=localhost:6379
MEANING_OF_LIFE=42
MY_VAR='Hello world'
создание модели настроек
class Settings(BaseSettings):
...
class Config:
env_file = '.env'
env_file_encoding = 'utf-8'
Создание инстанса настроек
settings = Settings(_env_file='prod.env', _env_file_encoding='utf-8')
Postponed annotations
Использование devtools
Смотри еще:
- attrs is the Python package that will bring back the joy of writing classes by relieving you from the drudgery of implementing object protocols (aka dunder methods)
- [pydantic-factories] This library offers powerful mock data generation capabilities for pydantic based models and dataclasses. It can also be used with other libraries that use pydantic as a foundation, for example SQLModel and Beanie.
- [sql-model]. SQLModel is a library for interacting with SQL databases from Python code, with Python objects. Используется в [databases] и [fastapi]
- [pydantic-validation-custom] Pydantic Settings management pydantic-computed а new decorator for pydantic allowing you to define dynamic fields that are computed from other properties.
- [mock-libraries]
- [2023-01-23-daily-note] закрытые атрибуты, женерики, корневые типы, корневые валидаторы, заполнение полей и ошибки в mypy
- awesome-pydantic A curated list of awesome things related to Pydantic!
- [devtools]
- [fastapi-setting-environment-variables] про поддержку .env
- [fastapi]
- How to parse ObjectId in a pydantic model?
- Using bson.ObjectId in Pydantic v2
- Remove an inherited field from a model in pydantic v2, Remove inherited/parent field from model