В одной из предыдущих статей (англ) я рассматривал dataclasses как способ написания классов python, которые выступают в качестве контейнеров данных.

Проект dataclasses показался мне потрясающим по нескольким причинам:

  • он реализует множество удобных методов, чтобы избавить вас от написания шаблонного кода;
  • обеспечивает лаконичный синтаксис;
  • задействует современные практики программирования.

Однако в этом модуле не хватает важной функции  —  валидации данных, то есть процесса, с помощью которого вы накладываете ограничения схемы на данные во время выполнения.

Вот тут нам и пригодится проект Pydantic. Мы рассмотрим 8 ее полезных функций и увидим, как можно быстро внедрить их в приложения с помощью нескольких строк кода.

Что такое валидация данных?

Валидация данных  —  это процесс, который приводит ваши данные в соответствие с набором правил, схем или ограничений, которые вы определили для каждого атрибута. Это заставляет код принимать и возвращать данные именно так, как вы того ожидаете.

Валидация данных предотвращает непредвиденные ошибки, возникающие из-за таких проблем, как неправильный ввод данных пользователем. В этом смысле она также выполняет роль функции очистки.

Приведем пример. Представьте, что вы создаете API кредитного скоринга для оценки кредитоспособности человека.

Для того чтобы этот API заработал, нужно отправить POST-запрос на конкретный URL и предоставить некоторые данные полезной нагрузки.

Эти данные определяют личность: некоторые поля являются обязательными. Но этим дело не ограничивается.

По какой-то причине это физическое лицо должно иметь французский налоговый идентификатор, указанный в одном из обязательных полей. Этот идентификатор составлен по определенной схеме: скажем, он должен состоять из 13 цифр и заканчиваться двумя заглавными буквами.

Человек должен указать свой полный адрес: название улицы, ее номер и почтовый индекс. Почтовый индекс должен состоять из 5 цифр.

Перечень можно продолжать и дальше, но суть вам понятна.

👉 Валидация данных гарантирует, что данные, которые вы отправляете в API, соответствуют данным ограничениям.

Вы можете привести свои данные в соответствие с этими ограничениям, загрузив их и применив ряд условий к каждому полю. Это может сработать, но вам придется написать большое количество кода, который со временем станет невозможно поддерживать.

Что если бы мы могли инкапсулировать данные в класс, создать типизированный атрибут для каждого поля и проверять ограничения полей во время выполнения, когда данные загружаются в класс?

Решить эту задачу нам поможет Pydantic.

Что такое Pydantic?

“Pydantic  —  это библиотека, которая обеспечивает проведение валидации данных и управление настройками с помощью аннотаций типов” (из официальной документации Pydantic).

Мне нравится воспринимать Pydantic как щепотку соли, которой вы посыпаете еду (или, в данном случае, вашу кодовую базу), чтобы сделать ее вкуснее. Pydantic все равно, как вы выполняете то или иное действие. Он скорее слой абстракции, который вы добавляете в свой код, не меняя его логики.

Этот слой, как вы, возможно, и ожидаете, будет в основном заниматься парсингом и валидацией данных, а также другими важными функциями.

Давайте рассмотрим 8 возможностей Pydantic, чтобы понять, чем он может быть нам полезен.

1. Простой синтаксис для определения моделей данных

Вы можете определить свои данные внутри класса, который наследует от класса BaseModel.

Модели Pydantic  —  это структуры, которые принимают данные, парсят их и проверяют, чтобы они соответствовали ограничениям полей, определенных в них.

Начнем с простого примера. Определим класс Person, который имеет два поля без каких-либо ограничений: first_name и last_name.

from pydantic import BaseModel

class Person(BaseModel):
first_name: str
last_name: str
  • Как и в случае с dataclasses, мы используем аннотации типов.
  • В отличие от dataclasses, мы не используем декоратор @dataclass. Вместо этого мы наследуем от класса BaseModel.

Вызвав модуль typing, мы можем добавлять поля с более сложными типами:

from pydantic import BaseModel
from typing import Optional, List

class Person(BaseModel):
first_name: str
last_name: str
interest: Optional[List[str]]

data = {"first_name": "Ahmed", "last_name": "Besbes"}
person = Person(**data)
print(person)

# first_name='Ahmed' last_name='Besbes' address=None interests=None

Вы даже можете создавать типы, которые сами являются классом BaseModel:

from pydantic import BaseModel
from typing import Optional, List

class Address(BaseModel):
street: str
number: int
zipcode: str

class Person(BaseModel):
first_name: str
last_name: str
interest: Optional[List[str]]

address_data = {"street": "Main street", "number": 1, "zipcode": 12345}
address = Address(**address_data)
data = {"first_name": "Ahmed", "last_name": "Besbes", "address": address}

person = Person(**data)
print(person)
# first_name='Ahmed' last_name='Besbes' address=Address(street='Main street', number=1, zipcode='12345') interests=None

2. Удобные для пользователя сообщения об ошибках

Что произойдет, если вы определите модель Pydantic и передадите ей данные, которые не соответствуют заданной схеме?

Чтобы понять, как поведет себя Pydantic в этом конкретном случае, давайте рассмотрим простую модель:

from pydantic import BaseModel
from typing import Optional

class Address(BaseModel):
street: str
number: int
zipcode: str

class Person(BaseModel):
first_name: str
last_name: str
age: int
address: Optional[Address]

Давайте теперь передадим ей некоторые несовместимые данные и посмотрим, какие сообщения об ошибках будут возвращены.

❌ Первый случай: отсутствует обязательное поле:

Давайте опустим поле age(возраст). Это немедленно вызывает ошибку ValidationError, которая указывает на конкретное отсутствующее поле.

Изображение автора

❌ Второй случай: несовместимый тип поля

Вместо целого числа в поле age передадим строку: например, "30 years"(“30 лет”) вместо 30. Аналогично, это вызовет ошибку ValidationError, а в сообщении будет явно указано, что поле ожидает целочисленный тип.

Изображение автора

👉 Стоит отметить, что Pydantic всегда пытается в принудительном порядке обработать тип, который вы аннотировали. Например, если попытаться передать “30” в поле age, несмотря на то, что это поле ожидает целочисленное значение, ошибки не будет. Pydantic справляется с этой ситуацией без проблем.

Изображение автора

👉 Вы можете сделать сообщения об ошибках Pydantic более явными, импортировав класс ValidationError и вызвав его внутри оператора try / except.

Изображение автора

3. Хорошая интеграция с IDE и линтерами

Pydantic хорошо интегрируется с современными IDE, такими как VSCode и PyCharm, что помогает быстро отлаживать код и избегать глупых ошибок. Например, когда вы инстанцируете объект из модели Pydantic, то сразу можете воспользоваться автозаполнением:

Функция автозаполнения из среды разработки. Скриншот автора

Доступен также линтинг, если вы используете статическую проверку типов, например mypy.

В следующем примере, если вы попытаетесь применить функцию len к атрибуту age, VSCode сообщит об ошибке через mypy.

Линтинг с помощью MyPy. Скриншот автора

4. Настройка полей

Pydantic снабжен нативным механизмом по добавлению валидаций для каждого поля с помощью обертывания внутри класса Field.

Например:

  • вы можете добавить ограничения на длину строковых полей с помощью аргументов Field max_length и min_length;
  • вы можете установить границы для числовых полей, используя аргументы Field ge и le ( ge  —  больше или равно, le  —  меньше или равно).

Рассмотрим следующий пример. Добавим ограничения в отношении полей first_name(длина от 2 до 20) и age(значение меньше 150).

from pydantic import BaseModel, Field
from typing import Optional

class Address(BaseModel):
street: str
number: int
zipcode: str

class Person(BaseModel):
first_name: str = Field(min_length=2, max_length=20)
last_name: str
age: int = Field(le=150)
address: Optional[Address]

Давайте посмотрим, какие ошибки вернутся, если мы попытаемся передать классу Person следующие данные:

data = {"first_name": "a", "last_name": "Besbes", "age": 200}
Ошибки после настройки полей. Скриншот автора

Вы можете выполнить более сложную настройку полей. Вот некоторые аргументы, которые используются внутри класса Field:

  • regex: добавляет валидатор регулярных выражений. Функция пригодится, когда вам понадобится соответствие некоторых строковых значений определенному шаблону.
  • multiple_of: применяется к полям int. Добавляет валидатор “multiple of”.
  • max_items и min_items: применяются к спискам и накладывают ограничение на количество содержащихся в них элементов.
  • allow_mutation: применяется к любому типу полей. По умолчанию установлено значение False. При установке значения True поле становится неизменяемым (или защищенным).

Чтобы узнать больше о широких возможностях настройки Pydantic Field, пройдите по этой ссылке из документации.

5. Множество вспомогательных методов

Вам не придется изобретать колесо. Pydantic предоставляет пользователю большое количество вспомогательных функций и методов. Приведу несколько примеров. Взгляните на код ниже:

from pydantic import BaseModel, Field
from typing import Optional

class Address(BaseModel):
street: str
number: int
zipcode: str

class Person(BaseModel):
first_name: str = Field(min_length=2, max_length=20)
last_name: str
age: int = Field(le=150)
address: Optional[Address]

Давайте создадим объект Person:

data = {"first_name": "Ahmed", "last_name": "Besbes", "age": 30}
person = Person(**data)

У этого объекта есть доступ ко многим полезным методам:

  • dict(): возвращает словарь из объекта;
  • json(): возвращает словарь в формате JSON;
  • copy(): возвращает копию объекта;
  • schema(): выводит схему в формате JSON.
Вспомогательные функции Pydantic. Скриншот автора

Чтобы узнать больше о вспомогательных функциях, перейдите по этой ссылке.

6. Типы Pydantic

str , int , float , List  —  это все обычные типы, с которыми мы работаем.

Но в некоторых случаях мы можем работать со значениями, которые требуют специальной валидации, например, пути, адреса электронной почты, IP-адреса и т.п.

К счастью, Pydatnic предоставляет список встроенных типов для многих из этих случаев использования:

  • FilePath: для парсинга путей к файлам;
  • DirectoryPath: для парсинга путей к каталогам;
  • EmailStr: для парсинга адресов электронной почты;
  • Color: для парсинга цветов HTML (см. тип цвета);
  • HttpUrl: для парсинга строгих HTTP URL-адресов;
  • IPvAnyAddress: для парсинга адресов IPv4 и IPv6.

7. Пользовательские валидаторы

Pydantic позволяет вам написать свой собственный валидатор. Допустим, мы хотим добавить поле PhoneNumber в предыдущий пример. Нам нужно, чтобы это поле соответствовало двум требованиям:

  • это строка из 10 цифр;
  • она должна начинаться с 0.

Чтобы выполнить пользовательскую валидацию, необходимо импортировать функцию validator из Pydantic и использовать ее в качестве декоратора перед функцией, проверяющей значение поля.

import re
from pydantic import BaseModel, validator
from typing import Optional

class Address(BaseModel):
street: str
number: int
zipcode: str

class Person(BaseModel):
first_name: str
last_name: str
phone_number: str
age: int
address: Optional[Address]

@validator("phone_number")
def phone_number_must_have_10_digits(cls, v):
match = re.match(r"0\d{9}", v)
if (match is None) or (len(v) != 10):
raise ValueError("Phone number must have 10 digits")
return v
Пользовательские валидаторы. Скриншот автора

Конечно, вы можете проводить более сложные валидации. Перейдите по этой ссылке, чтобы узнать больше.

8. Парсинг значений переменных среды

Pydantic позволяет читать переменные среды из файлов .env и парсить их непосредственно внутри класса BaseSettings. Для этого вам сначала нужно установить python-dotenv.

Предположим, что у вас есть некоторые переменные среды внутри этого файла .env:

LOGIN=Ahmed
API_KEY=SeCR€t!
SEED=42

Чтобы Pydantic загрузил эти переменные, нам сначала нужно определить класс Settings, который наследует от класса BaseSettings.

Внутри класса Settings мы определим переменные, которые перечислены в .env-файле, добавив при этом типы и валидаторы. И, наконец, мы укажем, что переменные среды должны быть прочитаны из файла .env.

from pydantic import BaseSettings

class Settings(BaseSettings):
api_key: str
login: str
seed: int
class Config:
env_file = ".env"
env_file_encoding = "utf-8"

settings = Settings()
print(settings)

Если мы запустим код, настройки будут выведены на терминал:

Загрузка переменных среды из файлов .env. Скриншот автора

Если мы заменим числовое обозначение “42” на буквенное “сорок два” в файле .env, вот что мы получим:

Скриншот автора

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Ahmed Besbes: 8 Reasons to Start Using Pydantic to Improve Data Parsing and Validation

Предыдущая статьяКак тестировать компоненты React
Следующая статьяLaravel: неизвестный, но эффективный способ реализации фильтров в Eloquent