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