Pydantic
Введение | |
Пример применения | |
BaseModel | |
Вывод сообщения об ошибке как JSON | |
Диапазон допустимых значений | |
@validator | |
@root_validator | |
Полный код примеров | |
Похожие статьи |
Введение
Pydantic это библиотека, с помощью которой можно парсить данные и выполнять валидацию.
Про установку можете прочитать
здесь
Свободный перевод того что они пишут о себе + комментарии:
Проверка данных и управление настройками с помощью аннотаций типа python.
pydantic применяет аннотации типа (type hints - смотрите
PEP 484
)
во время выполнения (runtime) и предоставляет понятные пользователю сообщения об ошибках, когда данные некорректны.
Определить какими должны быть данные можно с помощью в чистого, канонического python. Затем можно сделать валидацию с помощью pydantic.
Pydantic использует возможность современного Python. Убедитесь, что у вас версия не ниже 3.7
Желательно установить последнюю стабильную версию Python. Если нужно - прочитайте
«Руководство по установке Python в Linux»
Тем не менее, если вы планируете обмениваться данными в формате JSON со Swagger или Open API проверьте текущую
совместимость библиотек.
Пример использования
Рассмотрим скрипт PydanticDemo.py
from dataclasses import dataclass
from typing import Tuple
from enum import Enum
@dataclass
class IceCreamMix:
name: str
flavor: str
toppings: Tuple[str, ...]
scoops: int
def main():
ice_cream_mix = IceCreamMix(
"PB&J",
"peanut butter",
("strawberries", "sprinkles"),
2
)
print(ice_cream_mix)
if __name__ == '__main__':
main()
python PydanticDemo.py
IceCreamMix(name='PB&J', flavor='peanut butter', toppings=('strawberries', 'sprinkles'), scoops=2)
Этот скрипт успешно демонстрирует тип мороженого
Добавим ещё немного ООП
from dataclasses import dataclass
from typing import Tuple
from enum import Enum
class Flavor(str, Enum):
chocolate = 'chocolate'
vanilla = 'vanilla'
strawberry = 'strawberry'
mint = 'mint'
coffeee = 'coffee'
peanut_butter = 'peanut butter'
class Topping(str, Enum):
sprinkles = 'sprinkles'
hot_fudge = 'hot fudge'
cookies = 'cookies'
brownie = 'brownie'
whipped_cream = 'whipped cream'
strawberries = 'strawberries'
@dataclass
class IceCreamMix:
name: str
flavor: Flavor
toppings: Tuple[Topping, ...]
scoops: int
def main():
ice_cream_mix = IceCreamMix(
"PB&J",
Flavor.peanut_butter,
(Topping.strawberries, Topping.sprinkles),
2
)
print(ice_cream_mix)
if __name__ == '__main__':
main()
$ python PydanticDemo.py
IceCreamMix(name='PB&J', flavor=<Flavor.peanut_butter: 'peanut butter'>, toppings=(<Topping.strawberries: 'strawberries'>, <Topping.sprinkles: 'sprinkles'>), scoops=2)
Скрипт по-прежнему работает
Что если мы по ошибке выберем несуществующий запах или топпинг
def main():
ice_cream_mix = IceCreamMix(
"PB&J",
"smells like shit",
(Topping.strawberries, 111),
2
)
python PydanticDemo.py
IceCreamMix(name='PB&J', flavor='smells like shit', toppings=(<Topping.strawberries: 'strawberries'>, 111), scoops=2)
Скрипт не замечает подвоха.
Чтобы проверять данные автоматически установите pydantic и внесите всего одно изменение в первую строку
from pydantic.dataclasses import dataclass
python PydanticDemo.py
Traceback (most recent call last): File "PydanticDemo.py", line 41, in <module> main() File "PydanticDemo.py", line 31, in main ice_cream_mix = IceCreamMix( File "<string>", line 7, in __init__ File "C:\Users\Andrei\python\pydantic\venv\lib\site-packages\pydantic\dataclasses.py", line 99, in _pydantic_post_init raise validation_error pydantic.error_wrappers.ValidationError: 2 validation errors for IceCreamMix flavor value is not a valid enumeration member; permitted: 'chocolate', 'vanilla', 'strawberry', 'mint', 'coffee', 'peanut butter' (type=type_error.enum; enum_values=[<Flavor.chocolate: 'chocolate'>, <Flavor.vanilla: 'vanilla'>, <Flavor.strawberry: 'strawberry'>, <Flavor.mint: 'mint'>, <Flavor.coffeee: 'coffee'>, <Flavor.peanut_butter: 'peanut butter'>]) toppings -> 1 value is not a valid enumeration member; permitted: 'sprinkles', 'hot fudge', 'cookies', 'brownie', 'whipped cream', 'strawberries' (type=type_error.enum; enum_values=[<Topping.sprinkles: 'sprinkles'>, <Topping.hot_fudge: 'hot fudge'>, <Topping.cookies: 'cookies'>, <Topping.brownie: 'brownie'>, <Topping.whipped_cream: 'whipped cream'>, <Topping.strawberries: 'strawberries'>])
pydantic не пропустил наш код. Разберём выдачу подробнее
pydantic.error_wrappers.ValidationError: 2 validation errors for IceCreamMix
Указано количество ошибок и класс. Это помогло бы с дебагом, если бы мы не знали заранее где ошибки и сколько их
flavor value is not a valid enumeration member; permitted: 'chocolate', 'vanilla', 'strawberry', 'mint', 'coffee', 'peanut butter' (type=type_error.enum; enum_values=[<Flavor.chocolate: 'chocolate'>, <Flavor.vanilla: 'vanilla'>, <Flavor.strawberry: 'strawberry'>, <Flavor.mint: 'mint'>, <Flavor.coffeee: 'coffee'>, <Flavor.peanut_butter: 'peanut butter'>])
Pydantic подсказывает допустимые значения.
Тоже самое и с топпингами, где вместо допустимого значения стоит 111
toppings -> 1 value is not a valid enumeration member; permitted: 'sprinkles', 'hot fudge', 'cookies', 'brownie', 'whipped cream', 'strawberries' (type=type_error.enum; enum_values=[<Topping.sprinkles: 'sprinkles'>, <Topping.hot_fudge: 'hot fudge'>, <Topping.cookies: 'cookies'>, <Topping.brownie: 'brownie'>, <Topping.whipped_cream: 'whipped cream'>, <Topping.strawberries: 'strawberries'>])
Верните корректные значения для Flavor и Topping но замените scoops с 2 на '2'
def main():
ice_cream_mix = IceCreamMix(
"PB&J",
Flavor.peanut_butter,
(Topping.strawberries, Topping.sprinkles),
'2'
)
python PydanticDemo.py
IceCreamMix(name='PB&J', flavor=<Flavor.peanut_butter: 'peanut butter'>, toppings=(<Topping.strawberries: 'strawberries'>, <Topping.sprinkles: 'sprinkles'>), scoops=2)
scoops по-прежнему равно двум
Pydantic поддерживает приведение типа (type coercion)
BaseModel
Чтобы получить доступ к дополнительным возможностям таким как сериализация (Serialization) и поддержка JSON воспользуемся классом BaseModel
Просто напомню, что сперва у нас было
from dataclasses import dataclass
Затем
from pydantic.dataclasses import dataclass
А сейчас нужно сделать
from pydantic import BaseModel
И убрать декоратор @dataclass перед class IceCreamMix:
class IceCreamMix: нужно заменить на class IceCreamMix(BaseModel):
а также добавить имена аттрибутов код, создающий объект класса IceCreamMix
то есть name = "PB&J" flavor = Flavor.peanut_butter и так далее
strawberries = 'strawberries'
class IceCreamMix(BaseModel):
name: str
flavor: Flavor
toppings: Tuple[Topping, ...]
scoops: int
def main():
ice_cream_mix = IceCreamMix(
name = "PB&J",
flavor = Flavor.peanut_butter,
toppings = (Topping.strawberries, Topping.sprinkles),
scoops = 2
)
python PydanticDemo.py
IceCreamMix(name='PB&J', flavor=<Flavor.peanut_butter: 'peanut butter'>, toppings=(<Topping.strawberries: 'strawberries'>, <Topping.sprinkles: 'sprinkles'>), scoops=2)
Всё работает так же, как и до изменений.
Теперь можно вывести результат в виде JSON
print(ice_cream_mix.json())
python PydanticDemo.py
{"name": "PB&J", "flavor": "peanut butter", "toppings": ["strawberries", "brownie"], "scoops": 2}
Обратите внимание на JSON который вы получили выше.
Его можно скопировать, затем если нужно изменить и создать ещё один объект
прямо из JSON с помощью метода parse_raw()
Например:
another_mix = IceCreamMix.parse_raw('{"name": "New mix", "flavor": "mint", "toppings": ["cookies", "hot fudge"], "scoops": 2}')
print(another_mix.json())
{"name": "New mix", "flavor": "mint", "toppings": ["cookies", "hot fudge"], "scoops": 2}
Если случайно ошибиться со значением аттрибута - pydantic не даст соврать
another_mix = IceCreamMix.parse_raw('{"name": "New mix", "flavor": "novichoke", "toppings": ["cookies", "hot fudge"], "scoops": 2}')
print(another_mix.json())
python PydanticDemo.py
Traceback (most recent call last): File "/home/avorotyn/python/pydantic/PydanticDemo.py", line 45, in <module> main() File "/home/avorotyn/python/pydantic/PydanticDemo.py", line 40, in main another_mix = IceCreamMix.parse_raw('{"name": "New mix", "flavor": "novichoke", "toppings": ["cookies", "hot fudge"], "scoops": 2}') File "pydantic/main.py", line 543, in pydantic.main.BaseModel.parse_raw File "pydantic/main.py", line 520, in pydantic.main.BaseModel.parse_obj File "pydantic/main.py", line 362, in pydantic.main.BaseModel.__init__ pydantic.error_wrappers.ValidationError: 1 validation error for IceCreamMix flavor value is not a valid enumeration member; permitted: 'chocolate', 'vanilla', 'strawberry', 'mint', 'coffee', 'peanut butter' (type=type_error.enum; enum_values=[<Flavor.chocolate: 'chocolate'>, <Flavor.vanilla: 'vanilla'>, <Flavor.strawberry: 'strawberry'>, <Flavor.mint: 'mint'>, <Flavor.coffeee: 'coffee'>, <Flavor.peanut_butter: 'peanut butter'>])
ValidationError как JSON
В JSON можно также оформить сообщения об ошибках. Нужно импортировать из pydantic ValidationError и воспользоваться try: except
from pydantic import BaseModel, ValidationError
…
def main():
try:
ice_cream_mix = IceCreamMix(
name = "PB&J",
flavor = "spring",
toppings = (Topping.strawberries, Topping.sprinkles),
scoops = 2
)
except ValidationError as e:
print(e.json())
python PydanticDemo.py
[ { "loc": [ "flavor" ], "msg": "value is not a valid enumeration member; permitted: 'chocolate', 'vanilla', 'strawberry', 'mint', 'coffee', 'peanut butter'", "type": "type_error.enum", "ctx": { "enum_values": [ "chocolate", "vanilla", "strawberry", "mint", "coffee", "peanut butter" ] } } ]
Границы допустимых значений
Допустим вы хотите, чтобы число ложечек было обязательным аттрибутом со значениями от 0 до 5 не включая границы
from pydantic import BaseModel, ValidationError, Field
…
strawberries = 'strawberries'
class IceCreamMix(BaseModel):
name: str
flavor: Flavor
toppings: Tuple[Topping, ...]
scoops: int = Field(..., gt=0, lt=5)
python PydanticDemo.py
{"name": "PB&J", "flavor": "peanut butter", "toppings": ["strawberries", "brownie"], "scoops": 2}
Задано 2 ложечки, так что ошибок нет.
Попробуем 5 ложечек
def main():
try:
ice_cream_mix = IceCreamMix(
name = "PB&J",
flavor = "spring",
toppings = (Topping.strawberries, Topping.sprinkles),
scoops = 5
)
except ValidationError as e:
print(e.json())
python PydanticDemo.py
[ { "loc": [ "scoops" ], "msg": "ensure this value is less than 5", "type": "value_error.number.not_lt", "ctx": { "limit_value": 5 } } ] Traceback (most recent call last): File "/home/avorotyn/python/pydantic/PydanticDemo.py", line 50, in <module> main() File "/home/avorotyn/python/pydantic/PydanticDemo.py", line 41, in main print(ice_cream_mix.json()) UnboundLocalError: local variable 'ice_cream_mix' referenced before assignment
Не обращайте внимание на Traceback - можно было вложить print в try, но если всё ок, то объёкт создается и этой ошибки нет, а если не ок, то pydantic ловит несоответствие и выдает value_error
Валидация с помощью декоратора validator
Ещё один полезный способ установки ограничений - с помощью @validator
Он применяется если нужно следить за каким-то одним аттрибутом
from pydantic import BaseModel, ValidationError, Field, validator
…
class IceCreamMix(BaseModel):
name: str
flavor: Flavor
toppings: Tuple[Topping, ...]
scoops: int = Field(..., gt=0, lt=5)
@validator('toppings')
def check_toppings(cls, toppings):
if len(toppings) > 4:
raise ValueError('Too many toppings')
return toppings
Если запустить этот код с двумя топпингами никаких ошибок не будет, поэтому сразу добавим ещё три, чтобы в сумме стало пять.
def main():
try:
ice_cream_mix = IceCreamMix(
name = "PB&J",
flavor = "spring",
toppings = (Topping.strawberries, Topping.brownie,Topping.sprinkles,Topping.hot_fudge,Topping.whipped_cream),
python PydanticDemo.py
[ { "loc": [ "toppings" ], "msg": "Too many toppings", "type": "value_error" } ]
Теперь можно уменьшить число топпингов до четырёх и убедиться что ошибки нет.
@root_validator
Применяется когда нужно следить за всей моделью. Например за сочетаниями разных аттрибутов.
from pydantic import BaseModel, ValidationError, Field, validator, root_validator
…
Создайте ещё один класс - Container
strawberries = 'strawberries'
class Container(str, Enum):
cup = 'cup'
cone = 'cone'
waffle_cone = 'waffle cone'
class IceCreamMix(BaseModel):
name: str
flavor: Flavor
Зададим условие: если топпинг это hot_fudge то никакой рожок давать нельзя - можно только чашку (cup)
Валидацию будем делать через @root_validator
@validator('toppings')
def check_toppings(cls, toppings):
if len(toppings) > 4:
raise ValueError('Too many toppings')
return toppings
@root_validator
def check_cone_toppings(cls, toppings):
container = values.get('container')
toppings = values.get('toppings')
if container == Container.cone or container == Container.waffle_cone:
if Topping.hot_fudge in toppings:
raise ValueError('Cones cannot have hot fudge')
return values
def main():
try:
ice_cream_mix = IceCreamMix(
name = "PB&J",
container = Container.waffle_cone,
flavor = "spring",
У вас как раз должен был остаться топпинг hot fudge с прошлого примера, если нет - добавьте и запустите
python PydanticDemo.py
[ { "loc": [ "__root__" ], "msg": "Cones cannot have hot fudge", "type": "value_error" } ]
РЕКЛАМА от Яндекса. Может быть недоступна в вашем регионе
Конец рекламы. Если там пусто считайте это рекламой моей телеги
Окончательный код примера
Краткий обзор возможностей pydantic подошёл к концу.
Спасибо за внимание, ниже полный код к этой статье.
from pydantic import BaseModel, \
ValidationError, Field, validator, root_validator
from typing import Tuple
from enum import Enum
class Flavor(str, Enum):
chocolate = 'chocolate'
vanilla = 'vanilla'
strawberry = 'strawberry'
mint = 'mint'
coffeee = 'coffee'
peanut_butter = 'peanut butter'
class Topping(str, Enum):
sprinkles = 'sprinkles'
hot_fudge = 'hot fudge'
cookies = 'cookies'
brownie = 'brownie'
whipped_cream = 'whipped cream'
strawberries = 'strawberries'
class Container(str, Enum):
cup = 'cup'
cone = 'cone'
waffle_cone = 'waffle cone'
class IceCreamMix(BaseModel):
name: str
container: Container
flavor: Flavor
toppings: Tuple[Topping, ...]
scoops: int = Field(..., gt=0, lt=5)
@validator('toppings')
def check_toppings(cls, toppings):
if len(toppings) > 4:
raise ValueError('Too many toppings')
return toppings
@root_validator
def check_cone_toppings(cls, values):
container = values.get('container')
toppings = values.get('toppings')
if container == Container.cone or container == Container.waffle_cone:
if Topping.hot_fudge in toppings:
raise ValueError('Cones cannot have hot fudge')
return values
def main():
try:
ice_cream_mix = IceCreamMix(
name="PB&J",
container=Container.waffle_cone,
flavor=Flavor.peanut_butter,
# flavor='unknown flavour'
toppings=(Topping.strawberries, Topping.brownie,
Topping.sprinkles),
# на validator
# toppings=(Topping.strawberries, Topping.brownie,
# Topping.sprinkles,Topping.cookies, Topping.sprinkles),
# на root_validator
# toppings=(Topping.strawberries, Topping.brownie,
# Topping.sprinkles,Topping.hot_fudge),
scoops=2
# scoops=5
)
print(ice_cream_mix.json())
except ValidationError as e:
print(e.json())
if __name__ == '__main__':
main()
Pydantic models | |
Python | |
enumerate |
РЕКЛАМА от Яндекса. Может быть недоступна в вашем регионе
Конец рекламы. Если там пусто считайте это рекламой моей телеги