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
Типы:
Noneortype(None)orLiteral[None]boolintfloatstrbyteslist(принимает list, tuple, set, frozenset, deque и генераторы)tuple(принимает list, tuple, set, frozenset, deque и генераторы)dictset(принимает list, tuple, set, frozenset, deque и генераторы)frozenset(принимает list, tuple, set, frozenset, deque и генераторы)deque(принимает list, tuple, set, frozenset, deque и генераторы)datetime.datedatetime.timedatetime.datetimedatetime.timedeltatyping.Anyлюбое значениеtyping.Annotatedанотированное значениеtyping.TypeVarконстанта, базирующаяся на этомtyping.Unionнесколько разных типовtyping.Optionalобертка надUnion[x, None]typing.Listtyping.Tuple- subclass of
typing.NamedTuple - subclass of
collections.namedtuple typing.Dictи subclasstyping.Settyping.FrozenSettyping.Dequetyping.Sequencetyping.Iterableзарезевировано под другие итераторыtyping.Typetyping.Callabletyping.Patternдля regexipaddress.IPv4Addressи другие ip…enum.Enumи subclass ofenum.Enumenum.IntEnumи subclassdecimal.Decimalконвертит в строку, затем в decimalpathlib.Pathuuid.UUIDByteSize
Пример с итераторами
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
FilePathDirectoryPathEmailStrNameEmailPyObjectColorhtml/css цвет вот в таком формате. Поддерживается несколько методов конвертации.JsonПримерPaymentCardNumberПримерAnyUrlописание про урлыAnyHttpUrlHttpUrlPostgresDsnRedisDsnstricturlUUID1и 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_allextraзабыть, применить или игнорировать экстра атрибуты при инциализацииallow_mutationдля неизменяемых типовfrozenоткрывает дорогу к хешированию инстансов модели- …
orm_modeиспользовать как модель для ORMalias_generatorschema_extrajson_loadsкастомные ф-ии для jsonjson_dumpsjson_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