Библиотека Pydantic: валидация данных на Python

Лого Pydantic

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

Друзья, подписывайтесь на наш телеграм канал Pythonist. Там еще больше туториалов, задач и книг по Python.

Содержание

Библиотека Pydantic

Одной из главных особенностей Python является то, что это динамически типизированный язык. Динамическая типизация означает, что типы переменных определяются во время выполнения программы. Для сравнения — в статически типизированных языках типы явно объявляются во время компиляции.

Динамическая типизация отлично подходит для быстрой разработки и простоты использования, но для реальных приложений часто требуется более надежная проверка типов и валидация данных. В этом вам поможет библиотека Python Pydantic. Она быстро завоевала популярность, и сейчас это самая распространенная библиотека проверки данных для Python.

Знакомство с Pydantic

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

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

Отличительные особенности Pydantic:

  • Кастомизация. С помощью Pydantic можно проверять данные практически любого типа. Эта библиотека позволяет валидировать и сериализовать практически любой объект Python, от примитивных типов до высоко вложенных структур данных.
  • Гибкость. Pydantic дает вам контроль над строгостью проверки данных. В некоторых случаях вы можете захотеть принудительно привести входящие данные к правильному типу. Например, вы можете принять данные, которые должны иметь тип float, но получены как целое число. В других случаях вы можете захотеть строго придерживаться типов получаемых данных. Pydantic позволяет вам поступать и так, и эдак.
  • Сериализация. Вы можете сериализовать и десериализовать объекты Pydantic в виде словарей и строк JSON. Это означает, что можно легко преобразовывать объекты Pydantic в JSON и обратно. Эта возможность привела к созданию самодокументированных API и интеграции практически с любым инструментом, поддерживающим JSON-схемы.
  • Производительность. Благодаря основной логике валидации, написанной на языке Rust, Pydantic работает исключительно быстро. Это преимущество в производительности обеспечивает быструю и надежную обработку данных, особенно в высокопроизводительных приложениях, таких как REST API, которые должны масштабироваться до большого количества запросов.
  • Экосистема и промышленное внедрение. Pydantic является зависимостью многих популярных библиотек Python, таких как FastAPI, LangChain и Polars. Эту библиотеку также используют большинство крупнейших технологических компаний. Это свидетельствует о поддержке сообщества, надежности и устойчивости Pydantic.

Эти особенности делают Pydantic привлекательной библиотекой проверки данных. В нашем руководстве мы покажем их в действии.

Установка Pydantic

Pydantic доступна на PyPI, и вы можете установить ее с помощью pip. Откройте терминал или командную строку, создайте новое виртуальное окружение, а затем выполните следующую команду для установки Pydantic:

(venv) $ python -m pip install pydantic

Эта команда установит последнюю версию Pydantic из PyPI на вашу машину. Чтобы убедиться, что установка прошла успешно, запустите Python REPL и импортируйте Pydantic:

>>> import pydantic

Если импорт пройдет без ошибок, значит, библиотека установлена успешно, и теперь в вашей системе есть ядро Pydantic.

Добавление дополнительных зависимостей

Вместе с Pydantic можно установить и дополнительные зависимости. Например, в этой статье мы будем работать с проверкой электронной почты, поэтому вы можете включить в свою установку эти зависимости:

(venv) $ python -m pip install "pydantic[email]"

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

(venv) $ python -m pip install pydantic-settings

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

Использование моделей

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

Лучший способ разобраться во всем этом — создать свои собственные модели.

Работа с Pydantic BaseModels

Предположим, вы создаете приложение, используемое отделом кадров для управления информацией о сотрудниках. Вам нужен способ проверить, что информация о новом сотруднике представлена в правильной форме. Например, каждый сотрудник должен иметь ID, имя, email, дату рождения, зарплату, отдел и выбор льгот. Это идеальный вариант использования модели Pydantic!

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

from datetime import date
from uuid import UUID, uuid4
from enum import Enum
from pydantic import BaseModel, EmailStr

class Department(Enum):
    HR = "HR"
    SALES = "SALES"
    IT = "IT"
    ENGINEERING = "ENGINEERING"

class Employee(BaseModel):
    employee_id: UUID = uuid4()
    name: str
    email: EmailStr
    date_of_birth: date
    salary: float
    department: Department
    elected_benefits: bool

Сначала вы импортируете зависимости, необходимые для определения модели сотрудника. Затем вы создаете enum для представления различных отделов вашей компании и используете его для аннотирования поля department в модели сотрудника.

После этого вы определяете свою модель Employee, которая наследует от BaseModel и определяет имена и ожидаемые типы полей с помощью аннотаций. Вот описание каждого поля, которое вы определили в Employee, и того, как Pydantic проверяет его при создании объекта Employee:

  • employee_id. Это UUID сотрудника, информацию о котором вы хотите сохранить. Используя аннотацию UUID, Pydantic гарантирует, что это поле всегда будет валидным UUID. Каждому экземпляру Employee по умолчанию будет присвоен UUID, который вы указали, вызвав uuid4().
  • name. Имя сотрудника, которое Pydantic ожидает как строку.
  • email. Pydantic убедится, что email каждого сотрудника является валидным, используя под капотом библиотеку Python email-validator.
  • date_of_birth. Дата рождения каждого сотрудника должна быть валидной датой, поскольку аннотирована как date из модуля Python datetime. Если вы передадите в date_of_birth строку, Pydantic попытается разобрать ее и преобразовать в объект date.
  • salary. Это зарплата сотрудника, и ожидается, что это будет число с плавающей запятой.
  • department. Отдел каждого сотрудника должен быть или HR, или SALES, или IT, или ENGINEERING, как определено в Department enum.
  • elected_benefits: Это поле хранит информацию о том, есть ли у сотрудника льготы, и Pydantic ожидает, что это будет булево значение.

Самый простой способ создать объект Employee — это инстанцировать его, как и любой другой объект Python. Для этого откройте Python REPL и запустите следующий код:

>>> from pydantic_models import Employee

>>> Employee(
...     name="Chris DeTuma",
...     email="cdetuma@example.com",
...     date_of_birth="1998-04-02",
...     salary=123_000.00,
...     department="IT",
...     elected_benefits=True,
... )
Employee(
    employee_id=UUID('73636d47-373b-40cd-a005-4819a84d9ea7'),
    name='Chris DeTuma',
    email='cdetuma@example.com',
    date_of_birth=datetime.date(1998, 4, 2),
    salary=123000.0,
    department=<Department.IT: 'IT'>,
    elected_benefits=True
)

В этом блоке вы импортируете Employee и создаете объект со всеми необходимыми полями сотрудника. Pydantic успешно проверяет и принудительно обрабатывает переданные вами поля и создает корректный объект Employee. Обратите внимание, как Pydantic автоматически преобразует строку даты в объект date, а строку IT — в соответствующее перечисление Department.

Теперь давайте посмотрим, как Pydantic отреагирует, если попытаться передать экземпляру Employee недопустимые данные:

>>> Employee(
...     employee_id="123",
...     name=False,
...     email="cdetumaexamplecom",
...     date_of_birth="1939804-02",
...     salary="high paying",
...     department="PRODUCT",
...     elected_benefits=300,
... )

Traceback (most recent call last):
pydantic_core._pydantic_core.ValidationError: 7 validation errors for
Employee

employee_id
  Input should be a valid UUID, invalid length: expected length 32 for
  simple format, found 3 [type=uuid_parsing, input_value='123',
  input_type=str] For further information visit
  https://errors.pydantic.dev/2.6/v/uuid_parsing

name
  Input should be a valid string [type=string_type, input_value=False,
  input_type=bool] For further information visit
  https://errors.pydantic.dev/2.6/v/string_type

email
  value is not a valid email address: The email address is not valid.
  It must have exactly one @-sign. [type=value_error,
  input_value='cdetumaexamplecom', input_type=str]

date_of_birth
  Input should be a valid date or datetime, invalid date separator,
  expected `-` [type=date_from_datetime_parsing,
  input_value='1939804-02', input_type=str] For further information
  visit https://errors.pydantic.dev/2.6/v/date_from_datetime_parsing

salary
  Input should be a valid number, unable to parse string as a number
  [type=float_parsing, input_value='high paying', input_type=str]
  For further information visit
  https://errors.pydantic.dev/2.6/v/float_parsing

department
  Input should be 'HR', 'SALES', 'IT' or 'ENGINEERING'
  [type=enum, input_value='PRODUCT', input_type=str]

elected_benefits
  Input should be a valid boolean, unable to interpret input
  [type=bool_parsing, input_value=300, input_type=int]
  For further information visit
  https://errors.pydantic.dev/2.6/v/bool_parsing

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

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

BaseModel оснащена набором методов, которые позволяют легко создавать модели из других объектов, таких как словари и JSON. Например, если вы хотите создать объект Employee из словаря, вы можете использовать метод класса .model_validate():

>>> new_employee_dict = {
...     "name": "Chris DeTuma",
...     "email": "cdetuma@example.com",
...     "date_of_birth": "1998-04-02",
...     "salary": 123_000.00,
...     "department": "IT",
...     "elected_benefits": True,
... }

>>> Employee.model_validate(new_employee_dict)
Employee(
    employee_id=UUID('73636d47-373b-40cd-a005-4819a84d9ea7'),
    name='Chris DeTuma',
    email='cdetuma@example.com',
    date_of_birth=datetime.date(1998, 4, 2),
    salary=123000.0,
    department=<Department.IT: 'IT'>,
    elected_benefits=True
)

Здесь мы создаем new_employee_dict, словарь с полями сотрудников, и передаем его в .model_validate() для создания экземпляра Employee. Под капотом Pydantic проверяет каждую запись словаря, чтобы убедиться, что она соответствует ожидаемым данным. Если какие-либо данные окажутся невалидными, Pydantic выдаст ошибку тем же способом, который вы видели ранее. Вы также получите уведомление, если в словаре отсутствуют какие-либо поля.

То же самое можно сделать с объектами JSON, используя .model_validate_json():

>>> new_employee_json = """
...  {"employee_id":"d2e7b773-926b-49df-939a-5e98cbb9c9eb",
...  "name":"Eric Slogrenta",
...  "email":"eslogrenta@example.com",
...  "date_of_birth":"1990-01-02",
...  "salary":125000.0,
...  "department":"HR",
...  "elected_benefits":false}
...  """

>>> new_employee = Employee.model_validate_json(new_employee_json)
>>> new_employee
Employee(
    employee_id=UUID('d2e7b773-926b-49df-939a-5e98cbb9c9eb'),
    name='Eric Slogrenta',
    email='eslogrenta@example.com',
    date_of_birth=datetime.date(1990, 1, 2),
    salary=125000.0,
    department=<Department.HR: 'HR'>,
    elected_benefits=False
)

В этом примере new_employee_json — это корректная JSON-строка, в которой хранятся поля сотрудников, и вы используете .model_validate_json() для валидации и создания объекта Employee из new_employee_json.

Хотя это может показаться мелочью, возможность создавать и проверять модели Pydantic из JSON очень важна, потому что JSON — один из самых популярных способов передачи данных через Интернет. Это одна из причин, по которой FastAPI полагается на Pydantic при создании REST API.

Вы также можете сериализовать модели Pydantic в виде словарей и JSON:

>>> new_employee.model_dump()
{
    'employee_id': UUID('d2e7b773-926b-49df-939a-5e98cbb9c9eb'),
    'name': 'Eric Slogrenta',
    'email': 'eslogrenta@example.com',
    'date_of_birth': datetime.date(1990, 1, 2),
    'salary': 125000.0,
    'department': <Department.HR: 'HR'>,
    'elected_benefits': False
}

>>> new_employee.model_dump_json()
'{"employee_id":"d2e7b773-926b-49df-939a-5e98cbb9c9eb",
⮑"name":"Eric Slogrenta",
⮑"email":"eslogrenta@example.com",
⮑"date_of_birth":"1990-01-02",
⮑"salary":125000.0,
⮑"department":"HR",
⮑"elected_benefits":false}'

Здесь вы используете .model_dump() и .model_dump_json() для преобразования модели new_employee в словарь и строку JSON соответственно. Обратите внимание, что .model_dump_json() возвращает JSON-объект с date_of_birth и department, сохраненными в виде строк.

Хотя Pydantic уже проверил эти поля и преобразовал вашу модель в JSON, тот, кто будет использовать этот JSON, не будет знать, что date_of_birth должна быть валидной датой, а department должен быть категорией в вашем Department enum. Чтобы решить эту проблему, вы можете создать схему JSON на основе модели Employee.

JSON-схемы указывают, какие поля ожидаются и какие значения будут представлены в JSON-объекте. Можно считать, что это JSON-версия определения класса Employee. Вот как создается JSON-схема для Employee:

>>> Employee.model_json_schema()
{
    '$defs': {
        'Department': {
            'enum': ['HR', 'SALES', 'IT', 'ENGINEERING'],
            'title': 'Department',
            'type': 'string'
        }
    },
    'properties': {
        'employee_id': {
            'default': '73636d47-373b-40cd-a005-4819a84d9ea7',
            'format': 'uuid',
            'title': 'Employee Id',
            'type': 'string'
        },
        'name': {'title': 'Name', 'type': 'string'},
        'email': {
            'format': 'email',
            'title': 'Email',
            'type': 'string'
        },
        'date_of_birth': {
            'format': 'date',
            'title': 'Date Of Birth',
            'type': 'string'
        },
        'salary': {'title': 'Salary', 'type': 'number'},
        'department': {'$ref': '#/$defs/Department'},
        'elected_benefits': {'title': 'Elected Benefits', 'type': 'boolean'}
    },
    'required': [
        'name',
        'email',
        'date_of_birth',
        'salary',
        'department',
        'elected_benefits'
    ],
    'title': 'Employee',
    'type': 'object'
}

Когда вы вызываете .model_json_schema(), вы получаете словарь, представляющий JSON-схему вашей модели. Первая запись, которую вы видите, показывает значения, которые может принимать department. Вы также видите информацию о том, как должны быть отформатированы ваши поля. Например, согласно этой JSON-схеме, employee_id должен быть UUID, а date_of_birth — датой.

Вы можете преобразовать свою JSON-схему в JSON-строку с помощью json.dumps(), что позволит практически любому языку программирования проверить JSON-объекты, созданные вашей моделью Employee. Другими словами, Pydantic не только может проверять входящие данные и сериализовать их в виде JSON, но и предоставляет другим языкам программирования информацию, необходимую для проверки данных вашей модели с помощью JSON-схем.

Итак, теперь вы понимаете, как использовать BaseModel в Pydantic для проверки и сериализации данных. Далее вы узнаете, как использовать поля для дальнейшей настройки валидации.

Использование полей для персонализации и метаданных

Пока что ваша модель Employee проверяет тип данных каждого поля и гарантирует, что некоторые поля, такие как email, date_of_birth и department, имеют допустимые форматы. Но допустим, вы также хотите убедиться, что salary — это положительное число, name — не пустая строка, а email содержит доменное имя вашей компании. Для этого можно использовать класс Field из Pydantic.

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

from datetime import date
from uuid import UUID, uuid4
from enum import Enum
from pydantic import BaseModel, EmailStr, Field

class Department(Enum):
    HR = "HR"
    SALES = "SALES"
    IT = "IT"
    ENGINEERING = "ENGINEERING"

class Employee(BaseModel):
    employee_id: UUID = Field(default_factory=uuid4, frozen=True)
    name: str = Field(min_length=1, frozen=True)
    email: EmailStr = Field(pattern=r".+@example\.com$")
    date_of_birth: date = Field(alias="birth_date", repr=False, frozen=True)
    salary: float = Field(alias="compensation", gt=0, repr=False)
    department: Department
    elected_benefits: bool

Здесь мы импортируем Field вместе с другими зависимостями, которые мы использовали ранее, и присваиваем значения по умолчанию некоторым полям Employee. Вот разбивка параметров Field, которые мы использовали для добавления дополнительной валидации и метаданных к полям:

  • default_factory. Этот параметр используется для определения вызываемого объекта, который генерирует значения по умолчанию. В примере выше мы задали default_factory равным uuid4. Это вызовет функцию uuid4() для генерации случайного UUID для employee_id, когда это необходимо. Вы также можете использовать лямбда-функцию для большей гибкости.
  • frozen. Это булевский параметр, который вы можете установить, чтобы сделать ваши поля неизменяемыми. Это означает, что если значение frozen равно True, соответствующее поле не может быть изменено после инстанцирования модели. В этом примере с помощью параметра frozen сделаны неизменяемыми поля employee_id, name и date_of_birth.
  • min_length. С помощью min_length и max_length можно контролировать длину строковых полей. В приведенном выше примере имя должно содержать не менее одного символа.
  • pattern. Для строковых полей можно задать шаблон в виде regex-выражения. Например, когда вы используете regex-выражение в примере выше для email, Pydantic следит за тем, чтобы каждый email-адрес заканчивался на @example.com.
  • alias. Этот параметр можно использовать, когда вы хотите назначить псевдоним для своих полей. Например, псевдоним birth_date для date_of_birth и compensation для salary. Вы можете использовать эти псевдонимы при инстанцировании или сериализации модели.
  • gt. Этот параметр используется для установки минимального значения для числовых полей. Буквосочетание «gt» — это сокращение от «greater than», что переводится как «больше, чем». В данном примере значение gt=0 гарантирует, что зарплата всегда будет положительным числом. В Pydantic есть и другие числовые ограничения, например lt, что означает «less than» («меньше, чем»).
  • repr. Этот булевский параметр определяет, будет ли поле отображаться в представлении полей модели. В данном примере при печати экземпляра Employee вы не увидите date_of_birth или salary.

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

>>> from pydantic_models import Employee

>>> incorrect_employee_data = {
...     "name": "",
...     "email": "cdetuma@fakedomain.com",
...     "birth_date": "1998-04-02",
...     "salary": -10,
...     "department": "IT",
...     "elected_benefits": True,
... }

>>> Employee.model_validate(incorrect_employee_data)
Traceback (most recent call last):

pydantic_core._pydantic_core.ValidationError: 3 validation errors for
Employee
name
  String should have at least 1 character [type=string_too_short,
   input_value='', input_type=str] For further information visit
    https://errors.pydantic.dev/2.6/v/string_too_short
email
  String should match pattern '.+@example\.com$'
  [type=string_pattern_mismatch,
  input_value='cdetuma@fakedomain.com', input_type=str]
    For further information visit
    https://errors.pydantic.dev/2.6/v/string_pattern_mismatch
salary
  Input should be greater than 0 [type=greater_than, input_value=-10,
  input_type=int] For further information visit
  https://errors.pydantic.dev/2.6/v/greater_than

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

Теперь обратите внимание на дополнительные возможности, которые вы получаете при проверке корректных данных Employee:

>>> employee_data = {
...     "name": "Clyde Harwell",
...     "email": "charwell@example.com",
...     "birth_date": "2000-06-12",
...     "compensation": 100_000,
...     "department": "ENGINEERING",
...     "elected_benefits": True,
... }

>>> employee = Employee.model_validate(employee_data)
>>> employee
Employee(
    employee_id=UUID('614c6f75-8528-4272-9cfc-365ddfafebd9'),
    name='Clyde Harwell',
    email='charwell@example.com',
    department=<Department.ENGINEERING: 'ENGINEERING'>,
    elected_benefits=True)

>>> employee.salary
100000.0

>>> employee.date_of_birth
datetime.date(2000, 6, 12)

В этом блоке мы создаем словарь и модель Employee с помощью .model_validate(). В поле employee_data обратите внимание на то, что мы использовали birth_date вместо date_of_birth и compensation вместо salary. Pydantic распознает эти псевдонимы и внутренне присваивает их значения правильным именам полей.

Поскольку мы задали repr=False, salary и date_of_birth не отображаются в представлении Employee. Чтобы увидеть их значения, необходимо явно обратиться к ним как к атрибутам. Наконец, обратите внимание на то, что происходит при попытке изменить замороженное поле:

>>> employee.department = "HR"
>>> employee.name = "Andrew TuGrendele"
Traceback (most recent call last):

pydantic_core._pydantic_core.ValidationError: 1
validation error for Employee
name
  Field is frozen [type=frozen_field, input_value='Andrew TuGrendele',
  input_type=str]
  For further information visit
  https://errors.pydantic.dev/2.6/v/frozen_field

Здесь мы сначала меняем значение department с IT на HR. Это вполне допустимо, поскольку department не является замороженным полем. Однако, когда вы пытаетесь изменить name, Pydantic выдает ошибку, говоря, что name — это замороженное поле.

Итак, вы познакомились с классами BaseModel и Field в Pydantic. С их помощью вы можете определить множество различных правил валидации и метаданных для своих схем данных, но иногда этого недостаточно. Далее вы расширите возможности валидации полей с помощью валидаторов Pydantic.

Работа с валидаторами

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

С помощью валидаторов можно выполнить практически любую логику проверки, которую можно выразить в функции. Далее вы увидите, как это сделать.

Валидация моделей и полей

Продолжим использовать пример с сотрудниками. Предположим, что ваша компания придерживается политики, согласно которой на работу принимаются сотрудники не моложе восемнадцати лет. Каждый раз, когда вы создаете новый объект Employee, вам нужно убедиться, что сотрудник старше восемнадцати лет. Для этого можно добавить поле «age» и использовать класс Field, чтобы убедиться, что сотруднику не менее восемнадцати лет. Однако это кажется излишним, поскольку вы уже храните дату рождения сотрудника.

Лучшее решение — использовать валидатор полей Pydantic. Валидаторы полей позволяют применять пользовательскую логику проверки к полям BaseModel путем добавления методов класса в модель. Чтобы убедиться, что всем сотрудникам не меньше восемнадцати лет, вы можете добавить в модель Employee следующий валидатор поля:

from datetime import date
from uuid import UUID, uuid4
from enum import Enum
from pydantic import BaseModel, EmailStr, Field, field_validator

class Department(Enum):
    HR = "HR"
    SALES = "SALES"
    IT = "IT"
    ENGINEERING = "ENGINEERING"

class Employee(BaseModel):
    employee_id: UUID = Field(default_factory=uuid4, frozen=True)
    name: str = Field(min_length=1, frozen=True)
    email: EmailStr = Field(pattern=r".+@example\.com$")
    date_of_birth: date = Field(alias="birth_date", repr=False, frozen=True)
    salary: float = Field(alias="compensation", gt=0, repr=False)
    department: Department
    elected_benefits: bool

    @field_validator("date_of_birth")
    @classmethod
    def check_valid_age(cls, date_of_birth: date) -> date:
        today = date.today()
        eighteen_years_ago = date(today.year - 18, today.month, today.day)

        if date_of_birth > eighteen_years_ago:
            raise ValueError("Employees must be at least 18 years old.")

        return date_of_birth

В этом блоке мы импортируем field_validator и используем его для оформления метода класса в Employee под названием .check_valid_age(). Валидаторы полей должны быть определены как методы класса. В .check_valid_age() мы вычисляем сегодняшнюю дату, но восемнадцать лет назад. Если date_of_birth сотрудника окажется после этой даты, будет выдана ошибка.

Чтобы увидеть, как работает этот валидатор, посмотрите этот пример:

>>> from pydantic_models import Employee
>>> from datetime import date, timedelta

>>> young_employee_data = {
...     "name": "Jake Bar",
...     "email": "jbar@example.com",
...     "birth_date": date.today() - timedelta(days=365 * 17),
...     "compensation": 90_000,
...     "department": "SALES",
...     "elected_benefits": True,
... }

>>> Employee.model_validate(young_employee_data)
Traceback (most recent call last):

pydantic_core._pydantic_core.ValidationError:
1 validation error for Employee
birth_date
  Value error, Employees must be at least 18 years old.
  [type=value_error, input_value=datetime.date(2007, 4, 10),
  input_type=date]
  For further information visit
  https://errors.pydantic.dev/2.6/v/value_error

Здесь мы указываем birth_date на семнадцать лет раньше текущей даты. Когда вы вызываете .model_validate() для проверки данных young_employee_data, вы получаете ошибку, говорящую о том, что сотрудникам должно быть не менее восемнадцати лет.

Функция field_validator() в Pydantic позволяет произвольно настраивать валидацию полей. Однако field_validator() не подойдет, если вы хотите сравнить несколько полей друг с другом или проверить модель в целом. Для этого вам придется использовать валидаторы модели.

В качестве примера предположим, что ваша компания нанимает людей в IT-отдел только по контракту (т. е. не берет их в штат компании). В связи с этим IT-сотрудники не имеют права на льготы, и их поле elected_benefits должно быть False. Вы можете использовать функцию Pydantic model_validator(), чтобы обеспечить соблюдение этого ограничения:

from typing import Self
from datetime import date
from uuid import UUID, uuid4
from enum import Enum
from pydantic import (
    BaseModel,
    EmailStr,
    Field,
    field_validator,
    model_validator,
)

class Department(Enum):
    HR = "HR"
    SALES = "SALES"
    IT = "IT"
    ENGINEERING = "ENGINEERING"

class Employee(BaseModel):
    employee_id: UUID = Field(default_factory=uuid4, frozen=True)
    name: str = Field(min_length=1, frozen=True)
    email: EmailStr = Field(pattern=r".+@example\.com$")
    date_of_birth: date = Field(alias="birth_date", repr=False, frozen=True)
    salary: float = Field(alias="compensation", gt=0, repr=False)
    department: Department
    elected_benefits: bool

    @field_validator("date_of_birth")
    @classmethod
    def check_valid_age(cls, date_of_birth: date) -> date:
        today = date.today()
        eighteen_years_ago = date(today.year - 18, today.month, today.day)

        if date_of_birth > eighteen_years_ago:
            raise ValueError("Employees must be at least 18 years old.")

        return date_of_birth

    @model_validator(mode="after")
    def check_it_benefits(self) -> Self:
        department = self.department
        elected_benefits = self.elected_benefits

        if department == Department.IT and elected_benefits:
            raise ValueError(
                "IT employees are contractors and don't qualify for benefits"
            )
        return self

Здесь мы добавили к импортам тип Self из Python и model_validator() из Pydantic. Затем мы создали метод .check_it_benefits(), который выдает ошибку, если сотрудник принадлежит к отделу IT и поле elected_benefits равно True. Если в @model_validator установить mode как after, Pydantic будет ждать, пока после инстанцирования модели не будет запущен метод .check_it_benefits().


Примечание. Вы могли заметить, что .check_it_benefits() аннотирован пайтоновским типом Self. Это потому, что .check_it_benefits() возвращает экземпляр класса Employee, а тип Self является предпочтительной аннотацией для этого. Если вы используете версию Python меньше 3.11, вам придется импортировать тип Self из typing_extensions.


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

>>> from pydantic_models import Employee

>>> new_employee = {
...     "name": "Alexis Tau",
...     "email": "ataue@example.com",
...     "birth_date": "2001-04-012",
...     "compensation": 100_000,
...     "department": "IT",
...     "elected_benefits": True,
... }

>>> Employee.model_validate(new_employee)
Traceback (most recent call last):

pydantic_core._pydantic_core.ValidationError: 1 validation error for
Employee
  Value error, IT employees are contractors and don't qualify for
  benefits.
  [type=value_error, input_value={'name': 'Alexis Tau',
  ...elected_benefits': True},
  input_type=dict]
    For further information visit
    https://errors.pydantic.dev/2.6/v/value_error

Здесь мы пытаемся создать модель Employee с IT-отделом и параметром elected_benefits, установленным в True. При вызове .model_validate() Pydantic выдает ошибку, сообщая, что сотрудники IT-отдела не имеют права на льготы, поскольку работают по контракту.

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

Теперь у вас должна быть прочная основа для создания моделей Pydantic для ваших собственных случаев использования. Далее мы рассмотрим, как можно использовать Pydantic для проверки произвольных функций, а не только полей BaseModel.

Использование декораторов для проверки функций

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

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

Для этого вы пишете следующую функцию:

import time
from typing import Annotated
from pydantic import PositiveFloat, Field, EmailStr, validate_call

@validate_call
def send_invoice(
    client_name: Annotated[str, Field(min_length=1)],
    client_email: EmailStr,
    items_purchased: list[str],
    amount_owed: PositiveFloat,
) -> str:

    email_str = f"""
    Dear {client_name}, \n
    Thank you for choosing xyz inc! You
    owe ${amount_owed:,.2f} for the following items: \n
    {items_purchased}
    """

    print(f"Sending email to {client_email}...")
    time.sleep(2)

    return email_str

Сначала вы импортируете зависимости, необходимые для написания и аннотирования send_invoice(). Затем вы создаете функцию send_invoice(), декорированную @validate_call.

Перед выполнением send_invoice() @validate_call проверяет, что каждый инпут соответствует вашим аннотациям. В данном случае @validate_call проверяет, что client_name содержит хотя бы один символ, client_email правильно отформатирован, items_purchased представляет собой список строк, а amount_owed — положительный float.

Если один из инпутов не соответствует вашей аннотации, Pydantic выдаст ошибку, подобную той, что вы уже видели в BaseModel. Если все входные данные верны, send_invoice() создает строку и имитирует ее отправку клиенту с помощью time.sleep(2).


Примечание. Вы могли заметить, что client_name аннотировано пайтоновским типом Annotated. В целом, вы можете использовать Annotated, когда хотите предоставить метаданные об аргументе функции. Pydantic рекомендует использовать Annotated, когда вам нужно проверить аргумент функции, имеющий метаданные, указанные Field.

Но если вы используете default_factory для присвоения аргументу функции значения по умолчанию, вам следует присвоить аргумент непосредственно экземпляру Field. Пример этого можно посмотреть в документации Pydantic.


Чтобы увидеть @validate_call и send_invoice() в действии, откройте новый Python REPL и выполните следующий код:

>>> from validate_functions import send_invoice

>>> send_invoice(
...     client_name="",
...     client_email="ajolawsonfakedomain.com",
...     items_purchased=["pie", "cookie", 17],
...     amount_owed=0,
... )
Traceback (most recent call last):
pydantic_core._pydantic_core.ValidationError: 4 validation errors for
send_invoice
client_name
  String should have at least 1 character [type=string_too_short,
  input_value='', input_type=str]
    For further information visit
    https://errors.pydantic.dev/2.6/v/string_too_short
client_email
  value is not a valid email address: The email address is not valid.
  It must have exactly one @-sign. [type=value_error,
  input_value='ajolawsonfakedomain.com', input_type=str]
items_purchased.2
  Input should be a valid string [type=string_type, input_value=17,
  input_type=int]
    For further information visit
    https://errors.pydantic.dev/2.6/v/string_type
amount_owed
  Input should be greater than 0 [type=greater_than, input_value=0,
  input_type=int]
    For further information visit
    https://errors.pydantic.dev/2.6/v/greater_than

В этом примере мы импортируем send_invoice() и передаем недопустимые аргументы функции. @validate_call распознает это и выбрасывает ошибки, сообщая, что аргумент client_name должен содержать хотя бы один символ, client_email невалиден, items_purchased должен содержать строки, а amount_owed должен быть больше нуля.

При передаче корректных данных send_invoice() работает, как и ожидалось:

>>> email_str = send_invoice(
...     client_name="Andrew Jolawson",
...     client_email="ajolawson@fakedomain.com",
...     items_purchased=["pie", "cookie", "cake"],
...     amount_owed=20,
... )
Sending email to ajolawson@fakedomain.com...

>>> print(email_str)

    Dear Andrew Jolawson,

    Thank you for choosing xyz inc! You
    owe $20.00 for the following items:

    ['pie', 'cookie', 'cake']

Хотя @validate_call не так гибок, как BaseModel, вы все равно можете использовать его для мощной проверки аргументов функции. Это сэкономит вам много времени и позволит избежать написания шаблонной логики проверки типов и валидации. Если вы уже занимались этим, то знаете, насколько громоздким может быть написание утверждений для каждого аргумента функции. Для многих случаев использования @validate_call позаботится об этом за вас.

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

Управление настройками

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

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

Настройка приложений с помощью BaseSettings

pydantic-settings — это один из самых мощных способов управления переменными окружения в Python, который широко используется и рекомендуется такими популярными библиотеками, как FastAPI. Вы можете использовать pydantic-settings для создания моделей, подобных BaseModel, которые анализируют и проверяют переменные окружения.

Основной класс в pydantic-settingsBaseSettings, и он обладает всеми теми же функциями, что и BaseModel. Но если вы создаете модель, которая наследуется от BaseSettings, инициализатор модели будет пытаться считывать из переменных окружения любые поля, не переданные в качестве именованных аргументов.

Чтобы понять, как это работает, рассмотрим пример. Предположим, что ваше приложение подключается к базе данных и другому API-сервису. Учетные данные базы данных и ключ API могут меняться со временем и часто меняются в зависимости от того, в какой среде вы развертываете приложение. Чтобы справиться с этим, вы можете создать следующую модель BaseSettings:

from pydantic import HttpUrl, Field
from pydantic_settings import BaseSettings

class AppConfig(BaseSettings):
    database_host: HttpUrl
    database_user: str = Field(min_length=5)
    database_password: str = Field(min_length=10)
    api_key: str = Field(min_length=20)

В этом скрипте мы импортируем зависимости, необходимые для создания модели BaseSettings. Обратите внимание, что мы импортируем BaseSettings из pydantic_settings с подчеркиванием вместо тире. Затем мы определяем модель AppConfig, которая наследуется от BaseSettings и хранит поля о нашей базе данных и ключе API. В этом примере database_host должен быть действительным HTTP URL, а остальные поля имеют ограничение на минимальную длину.

Далее откройте терминал и добавьте следующие переменные окружения. Если вы работаете в Linux, macOS или Windows Bash, вы можете сделать это с помощью команды export:

(venv) $ export DATABASE_HOST="http://somedatabaseprovider.us-east-2.com"
(venv) $ export DATABASE_USER="username"
(venv) $ export DATABASE_PASSWORD="asdfjl348ghl@9fhsl4"
(venv) $ export API_KEY="ajfsdla48fsdal49fj94jf93-f9dsal"

Вы также можете установить переменные окружения в Windows PowerShell. Затем вы можете открыть новый Python REPL и создать AppConfig:

>>> from settings_management import AppConfig

>>> AppConfig()
AppConfig(
    database_host=Url('http://somedatabaseprovider.us-east-2.com/'),
    database_user='username',
    database_password='asdfjl348ghl@9fhsl4',
    api_key='ajfsdla48fsdal49fj94jf93-f9dsal'
)

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

Далее закройте Python REPL и создайте невалидные переменные окружения:

(venv) $ export DATABASE_HOST="somedatabaseprovider.us-east-2"
(venv) $ export DATABASE_USER="usee"
(venv) $ export DATABASE_PASSWORD="asdf"
(venv) $ export API_KEY="ajf"

Теперь откройте другой Python REPL и заново запустите AppConfig:

>>> from settings_management import AppConfig

>>> AppConfig()
Traceback (most recent call last):
pydantic_core._pydantic_core.ValidationError: 4 validation errors for
AppConfig
database_host
  Input should be a valid URL, relative URL without a base
  [type=url_parsing, input_value='somedatabaseprovider.us-east-2',
  input_type=str]
    For further information visit
    https://errors.pydantic.dev/2.6/v/url_parsing
database_user
  String should have at least 5 characters [type=string_too_short,
  input_value='usee', input_type=str]
    For further information visit
    https://errors.pydantic.dev/2.6/v/string_too_short
database_password
  String should have at least 10 characters [type=string_too_short,
  input_value='asdf', input_type=str]
    For further information visit
    https://errors.pydantic.dev/2.6/v/string_too_short
api_key
  String should have at least 20 characters [type=string_too_short,
  input_value='ajf', input_type=str]
    For further information visit
    https://errors.pydantic.dev/2.6/v/string_too_short

Теперь при попытке создать AppConfig pydantic-settings выдает ошибку, говоря, что database_host не является действительным URL, а остальные поля не соответствуют минимальному ограничению по длине.

Хотя это был упрощенный пример конфигурации, вы можете использовать BaseSettings для парсинга и проверки практически всего, что вам нужно из переменных окружения. Любую проверку, которую можно выполнить с помощью BaseModel, можно выполнить и с помощью BaseSettings, включая пользовательскую проверку с помощью валидаторов модели и полей.

Наконец, давайте разберем, как еще лучше настроить поведение BaseSettings с помощью SettingsConfigDict.

Кастомизация настроек с помощью SettingsConfigDict

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

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

from pydantic import HttpUrl, Field
from pydantic_settings import BaseSettings, SettingsConfigDict

class AppConfig(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        case_sensitive=True,
        extra="forbid",
    )

    database_host: HttpUrl
    database_user: str = Field(min_length=5)
    database_password: str = Field(min_length=10)
    api_key: str = Field(min_length=20)

Этот сценарий такой же, как и в предыдущем примере, только на этот раз мы импортировали SettingsConfigDict и инициализировали его в AppConfig. В SettingsConfigDict мы указали, что переменные окружения должны считываться из файла .env, должна соблюдаться чувствительность к регистру, а дополнительные переменные окружения в файле .env запрещены.

Далее создайте файл с именем .env в том же каталоге, что и settings_management.py, и заполните его следующими переменными окружения:

database_host=http://somedatabaseprovider.us-east-2.com/
database_user=username
database_password=asdfjfffffl348ghl@9fhsl4
api_key=ajfsdla48fsdal49fj94jf93-f9dsal

Теперь вы можете открыть Python REPL и инициализировать вашу модель AppConfig:

>>> from settings_management import AppConfig

>>> AppConfig()
AppConfig(
    database_host=Url('http://somedatabaseprovider.us-east-2.com/'),
    database_user='username',
    database_password='asdfjfffffl348ghl@9fhsl4',
    api_key='ajfsdla48fsdal49fj94jf93-f9dsal'
)

Как видите, AppConfig успешно разобрала и проверила переменные окружения в вашем файле .env.

Наконец, добавьте несколько невалидных переменных в ваш файл .env:

DATABASE_HOST=http://somedatabaseprovider.us-east-2.com/
database_user=username
database_password=asdfjfffffl348ghl@9fhsl4
api_key=ajfsdla48fsdal49fj94jf93-f9dsal
extra_var=shouldntbehere

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

>>> from settings_management import AppConfig

>>> AppConfig()
Traceback (most recent call last):
pydantic_core._pydantic_core.ValidationError: 3 validation errors for
AppConfig
database_host
  Field required [type=missing, input_value={'database_user':
  'userna..._var': 'shouldntbehere'}, input_type=dict]
    For further information visit
    https://errors.pydantic.dev/2.6/v/missing
DATABASE_HOST
  Extra inputs are not permitted [type=extra_forbidden,
  input_value='http://somedatabaseprovider.us-east-2.com/', input_type=str]
    For further information visit
    https://errors.pydantic.dev/2.6/v/extra_forbidden
extra_var
  Extra inputs are not permitted [type=extra_forbidden,
  input_value='shouldntbehere', input_type=str]
    For further information visit
    https://errors.pydantic.dev/2.6/v/extra_forbidden

Мы получаем хороший список ошибок, говорящих о том, что database_host отсутствует и что у нас есть лишние переменные окружения в нашем .env-файле. Обратите внимание, что из-за ограничения чувствительности к регистру наша модель считает, что DATABASE_HOST является дополнительной переменной наряду с extra_var.

С помощью SettingsConfigDict и BaseSettings можно сделать гораздо больше, но эти примеры должны дать вам представление о том, как можно использовать pydantic-settings для управления переменными окружения в вашем конкретном случае.

Заключение

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

Перевод статьи «Pydantic: Simplifying Data Validation in Python».

Прокрутить вверх