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

Интересный факт: более 30% всех Python-проектов используют Pydantic, даже если это не всегда заметно на первый взгляд. А такие фреймворки, как FastAPI, вообще строят свою логику на основе Pydantic, делая его неотъемлемой частью своих решений.

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

Что вы узнаете к концу этой статьи:

  • Что такое Pydantic и его основное назначение.

  • Понятие модели в Pydantic.

  • Детально изучим, что такое поля и как встроенные механизмы Pydantic помогают в валидации данных.

  • Разберем кастомную валидацию полей (через field_validator) и глобальную валидацию на уровне модели (model_validator).

  • Разберемся с вопросом автогенерируемых поле в Pydantic

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

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

  • Узнаем, как интегрировать Pydantic с ORM-моделями (на примере SQLAlchemy, хотя это применимо и к другим ORM).

  • Научимся преобразовывать данные в удобные форматы — словари и JSON-строки.

В итоге: вы познакомитесь с Pydantic версии 2 и освоите основные его методы и подходы.

Краткий теоретический блок

Перед тем, как перейти к практике, давайте познакомимся с основными понятиями и возможностями Pydantic 2. Этот блок будет теоретическим, но крайне важным — все его аспекты мы затем закрепим на практике.

Я постараюсь объяснить всё максимально подробно и "дотошно", чтобы не оставалось вопросов.

Что такое Pydantic?

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

Основные функции Pydantic:

  • Валидация данных: проверка входных данных на соответствие ожидаемым типам и ограничениям.

  • Трансформация данных: автоматическое приведение данных к нужным типам и форматам.

Модели в Pydantic

Модели в Pydantic наследуются от класса BaseModel. Каждая модель описывает набор полей, которые представляют собой структуру данных и условия для их валидации.

Описание полей:

  • Типизация: Поля в модели описываются с указанием типов, например, name: str. Это обеспечивает базовую типовую валидацию.

  • Использование Field(): Позволяет аннотировать поля с дополнительными параметрами, такими как значения по умолчанию, ограничения и другие настройки.

Пример базовой модели:

from pydantic import BaseModel, Field

class User(BaseModel):
    name: str
    email: str = Field(..., alias='email_address')

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

  1. Минимальная валидация типов: Используя встроенные типы Python (например, str, int), можно проводить базовую проверку полей.

  2. Использование валидаторов: В Pydantic доступны валидаторы, такие как EmailStr для проверки email-адресов. Для использования расширенных валидаторов требуется установка дополнительных зависимостей: pydantic[email] или pydantic[all].

Пример с валидатором:

from pydantic import BaseModel, EmailStr

class User(BaseModel):
    name: str
    email: EmailStr

Декораторы в Pydantic

В версии Pydantic 2 добавлены новые возможности для валидации и вычисления полей с помощью декораторов.

  • @field_validator — заменяет старый @validator и позволяет добавлять кастомную логику валидации поля. Вызывается при создании или изменении модели.

Пример использования @field_validator:

from pydantic import BaseModel, field_validator

class User(BaseModel):
    age: int

    @field_validator('age')
    def check_age(cls, value):
        if value < 18:
            raise ValueError('Возраст должен быть больше 18 лет')
        return value
  • @computed_field — поле, которое вычисляется на основе других данных в модели. Его можно использовать для автоматической генерации значений, а также для валидации.

Пример использования @computed_field:

from pydantic import BaseModel, computed_field

class User(BaseModel):
    name: str
    surname: str

    @computed_field
    def full_name(self) -> str:
        return f"{self.name} {self.surname}"

Работа с ORM

Pydantic поддерживает интеграцию с ORM (например, SQLAlchemy) для валидации и трансформации данных, полученных из базы данных.

  • Чтобы настроить модель для работы с ORM, используйте параметр ConfigDict с флагом from_attributes=True.

Пример:

from datetime import date
from pydantic import BaseModel, ConfigDict

class User(BaseModel):
    id: int
    name: str = 'John Doe'
    birthday_date: date

    config = ConfigDict(from_attributes=True)
  • Для создания модели Pydantic из объекта ORM используется метод from_orm.

Пример:

user = User.from_orm(orm_instance)

Методы для работы с данными

  • dict() / model_dump() — преобразуют модель в словарь Python. В версии 2 метод model_dump() стал аналогом dict().

Пример:

data = user.model_dump()
  • json() / model_dump_json() — преобразуют модель в JSON-строку. В новой версии метод model_dump_json() заменяет старый json().

Пример:

json_data = user.model_dump_json()

Передача данных в модель

  1. Именованные аргументы: Поля модели могут задаваться напрямую при создании экземпляра.

Пример:

user = User(name="Oleg", age=30)
  1. Распакованные словари: Можно передать значения полей с помощью распаковки словарей **.

Пример:

user_data = {"name": "Oleg", "age": 30} user = User(**user_data)

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

Теперь закрепим все на практике.

Приступаем к практике Pydantic 2

Для начала создайте новый проект в вашей любимой IDE, например, PyCharm. Если у вас ещё нет установленного Pydantic, выполните установку, используя следующую команду:

pip install -U pydantic[all]

Что делает эта команда:

  • -U — обновляет Pydantic до последней версии, если он уже установлен, или просто установит последнюю доступную версию, если Pydantic отсутствует.

  • [all] — этот флаг добавляет всевозможные дополнительные модули и валидаторы, которые могут быть полезны в проекте, такие как валидатор email-адресов и другие расширенные функции.

Теперь, когда установка завершена, мы готовы приступить к практике по Pydantic 2.

Опишем первую модель Pydantic

from datetime import date
from pydantic import BaseModel


class User(BaseModel):
    id: int
    name: str
    birthday_date: date

В этой модели Pydantic мы описали три обязательных поля: id, name и birthday_date (день рождения). Эти поля будут валидироваться автоматически.

Давайте теперь создадим объект класса User, передав необходимые параметры.

oleg = User(id=1, 
            name='Oleg', 
            birthday_date=date(year=1993, month=2, day=19))

Тут все понятно и мы пока никаких отличий от работы с обычным классом не видим. Сейчас получим данные в виде словаря и JSON-строки, а после я покажу вам несколько трюков.

Для трансформации модели в Python-словарь (dict) можно использовать метод dict() или его полный аналог model_dump().

Для трансформации в JSON-строку можно использовать метод json() или model_dump_json().

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

to_dict = oleg.model_dump()
to_json = oleg.model_dump_json()

print(to_dict, type(to_dict))
print(to_json, type(to_json))
 Вывел тип объекта в консоль, чтоб были видны отличия.
Вывел тип объекта в консоль, чтоб были видны отличия.

А что будет если мы создадим объект другого пользователя и передадим данные следующим образом?

alex = User(id="2",
            name='Алексей',
            birthday_date="1990-11-22")

Первое что приходит в голову: «Конечно ошибка!». Что логично, ведь мы передали ID-пользователя строкой, когда ожидается целое число, а в поле birthday_date мы передали строку вместо объекта даты, но мы проверим.

alex = User(id="2",
            name='Алексей',
            birthday_date="1990-11-22")

to_dict = alex.model_dump()
to_json = alex.model_dump_json()

print(to_dict, type(to_dict))
print(to_json, type(to_json))
 Мы не получили ошибки!
Мы не получили ошибки!

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

Теперь создадим ещё одного пользователя и опишем его следующим образом:

dima = User(id="3",
            name=156,
            birthday_date="1990-11-22")

to_dict = dima.model_dump()
to_json = dima.model_dump_json()

print(to_dict, type(to_dict))
print(to_json, type(to_json))

По логике вещей Сейчас всё должно работать правильно, не так ли? Мы уже проверили, что строка преобразуется в целое число, а дата, переданная в виде строки, — в формат даты. Давайте убедимся в этом.

 Ошибка!
Ошибка!

Неожиданно, но мы получили ошибку. Давайте разберемся почему.

Почему произошла ошибка?

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

  • Если модель ожидает int, а мы передаём строку «123», Pydantic может преобразовать её в int.

  • Если модель ожидает str, но вы передаете int, как в нашем случае, Pydantic не пытается выполнить автоматическое приведение типов, так как не всегда очевидно, что целое число можно корректно интерпретировать как строку.

Выходов из этой ситуации две:

  1. Стараться передавать корректные данные для неоднозначных случаев

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

Валидатор полей (field_validator) в Pydantic

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

Назначение декоратора

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

Импорт декоратора

Для начала импортируем необходимые элементы из Pydantic:

from pydantic import BaseModel, field_validator
from datetime import date

Пример использования

Создадим модель User, добавив валидатор для поля name:

class User(BaseModel):
    id: int
    name: str
    birthday_date: date

    @field_validator('name', mode='before')
    def validate_name(cls, v):
        return str(v)

Декоратор field_validator всегда принимает один обязательный аргумент - название поля, которое необходимо валидировать. Второй аргумент, который предпочтительно указывать, mode.

Здесь важно отметить использование параметра mode='before'. Это указывает Pydantic на необходимость выполнения валидации и преобразования данных до создания экземпляра модели, а не после. Другой вариант - mode='after'.

Сам метод приничает само поле (v) и далее начинает его валидировать. Выше рассмотрен простой пример, но совсем скоро мы его усложним.

Теперь наш валидатор для поля name будет автоматически преобразовывать любые переданные значения в строку.

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

@field_validator('name', mode='before')
@classmethod
def validate_name(cls, v):
        return str(v)

Проверим модель

Попробуем создать экземпляр модели с такими данными:

user_data = {'id': 3, 'name': '156', 'birthday_date': '1990-11-22'}
user = User(**user_data)
print(user.dict())

Результат:

{'id': 3, 'name': '156', 'birthday_date': datetime.date(1990, 11, 22)}

Ошибок нет, данные успешно преобразовались и сериализовались в JSON. Однако здесь есть одна проблема — наш валидатор использован только для преобразования данных, а не для их проверки.

Неожиданный результат

Допустим, мы передадим такие данные:

dima = User(
    id="3",
    name=("Коля", True, False, 0, 19933),
    birthday_date="1990-11-22"
)

Результат:

{'id': 3, 'name': "('Коля', True, False, 0, 19933)", 'birthday_date': datetime.date(1990, 11, 22)}

Ошибки нет, но явно что-то не так — вместо строки в поле name мы получили кортеж, который был просто преобразован в строку. Очевидно, что такая ситуация недопустима.

Исправление валидации

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

@field_validator('name', mode='before')
def validate_name(cls, v):
    if isinstance(v, int):
        return str(v)
    elif isinstance(v, str):
        return v
    else:
        raise ValueError("Имя должно быть строкой или числом")

Теперь наш валидатор проверяет, является ли переданное значение строкой или числом. Если значение не подходит, вызывается исключение ValueError.

Проверка исправления

Попробуем снова передать некорректные данные:

dima = User(
    id="3",
    name=("Коля", True, False, 0, 19933),
    birthday_date="1990-11-22"
)

Результат:

Произошла ошибка: 1 validation error for User
name
  Value error, Имя должно быть строкой или числом [type=value_error, input_value=('Коля', True, False, 0, 19933), input_type=tuple]
    For further information visit https://errors.pydantic.dev/2.9/v/value_error

Теперь валидатор сработал правильно — он остановил создание объекта и вернул ошибку, указав, что переданное значение некорректно.

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

Валидатор модели (model_validator) в Pydantic

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

Основные особенности декоратора @model_validator:

  • Выполняется после валидации отдельных полей.

  • Имеет доступ ко всем полям модели одновременно.

  • Может изменять значения полей или всю модель целиком.

  • Используется для сложных проверок, связанных с несколькими полями.

Пример использования model_validator

Расширим наш класс User, добавив валидатор модели для проверки возраста пользователя и установления имени по умолчанию:

from pydantic import BaseModel, field_validator, model_validator
from datetime import date

class User(BaseModel):
    id: int
    name: str
    birthday_date: date

    @field_validator('name', mode='before')
    def validate_name(cls, v):
        if isinstance(v, int):
            return str(v)
        elif isinstance(v, str):
            return v
        else:
            raise ValueError("Имя должно быть строкой или числом")

    @model_validator(mode='after')
    def check_age(self):
        today = date.today()
        age = today.year - self.birthday_date.year - (
            (today.month, today.day) < (self.birthday_date.month, self.birthday_date.day))

        if age < 18:
            raise ValueError("Пользователь должен быть старше 18 лет")
        if age > 120:
            raise ValueError("Возраст не может превышать 120 лет")
        return self

    @model_validator(mode='after')
    def set_default_name(self):
        if self.name.strip() == '':
            self.name = f"User_{self.id}"
        return self

В этом примере:

  • Метод check_age проверяет, что возраст пользователя больше 18 лет, но меньше 120 лет. Эта проверка требует доступа к полю birthday_date и текущей дате, поэтому она реализована как валидатор модели.

  • Метод set_default_name устанавливает имя по умолчанию, если поле name пустое. Этот валидатор использует несколько полей (имя и идентификатор), поэтому также реализован на уровне модели.

Оба валидатора используют режим after, что означает, что они выполняются после валидации отдельных полей.

Пример использования:

try:
    user = User(id=1, name="John", birthday_date=date(2000, 1, 1))
    print(user)
except ValueError as e:
    print(f"Ошибка: {e}")

try:
    user = User(id=2, name="", birthday_date=date(2010, 1, 1))
    print(user)
except ValueError as e:
    print(f"Ошибка: {e}")

try:
    user = User(id=3, name="Alice", birthday_date=date(1900, 1, 1))
    print(user)
except ValueError as e:
    print(f"Ошибка: {e}")

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

Вычисляемые поля (computed_field)

Декоратор @computed_field позволяет создавать поля, которые вычисляются "на лету" при доступе к ним. Это полезно, когда нужно автоматически получать значения, основанные на других полях модели.

Пример с вычисляемыми полями:

Добавим вычисляемые поля full_name и age в наш класс User:

from pydantic import BaseModel, computed_field
from datetime import date
from dateutil.relativedelta import relativedelta

class User(BaseModel):
    id: int
    name: str
    surname: str
    birthday_date: date

    @computed_field
    def full_name(self) -> str:
        return f"{self.name} {self.surname}"

    @computed_field
    def age(self) -> str:
        today = date.today()
        delta = relativedelta(today, self.birthday_date)
        return f"{delta.years} лет, {delta.months} месяцев и {delta.days} дней"
  • Поле full_name вычисляется через объединение имени и фамилии.

  • Поле age рассчитывает возраст пользователя в годах, месяцах и днях с использованием библиотеки модуляrelativedelta библиотеки dateutil.

Пример использования вычисляемых полей:

alex = User(id=1, name="Алексей", surname="Яковенко", birthday_date="1993-02-19")
print(alex.dict())

Результат:

{'id': 1, 'name': 'Алексей', 'surname': 'Яковенко', 'birthday_date': datetime.date(1993, 2, 19), 'full_name': 'Алексей Яковенко', 'age': '31 лет, 7 месяцев и 28 дней'}

Как видите, вычисляемые поля работают автоматически, обеспечивая удобное получение производных значений.

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

Вычисляемые поля можно комбинировать с model_validate. В таком формате. Мы создаем новое поле а после, при помощи model_validate можно проверить все ли у нас корректно. Кроме того, проверку можно сделать и на этапе создания самого вычисляемого поля.

Для закрепления этого блока потренеруйтесь самостоятельно в использовании декораторов: model_validate, field_validate и computed_field.

Аннотируемые поля в Pydantic 2: использование функции Field

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

Импортируем необходимые модули:

from pydantic import BaseModel, Field

Что такое Field?

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

  • default: устанавливает значение по умолчанию для поля.

  • default_factory: функция, возвращающая значение по умолчанию.

  • alias: альтернативное имя поля для сериализации и десериализации.

  • title: заголовок поля для документации.

  • description: описание поля для документации.

  • exclude: исключает поле из сериализации.

  • repr: определяет, будет ли поле включено в строковое представление модели.

Пример простого использования Field

Допустим, у нас есть модель пользователя, где нужно задать значения по умолчанию и добавить немного метаданных:

class User(BaseModel):
    id: int = Field(default=1, description="Уникальный идентификатор пользователя")
    name: str = Field(default="John Doe", title="Имя пользователя", description="Полное имя")
    role: str = Field(default="user", alias="user_role", description="Роль пользователя в системе")

В этом примере:

  • Поле id имеет значение по умолчанию 1 и описание для документации.

  • Поле name описывает имя пользователя с указанием заголовка для документации.

  • Поле role имеет алиас user_role, который будет использоваться при сериализации и десериализации данных.

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

Функция Field также позволяет задавать различные ограничения для значений. Например, можно определить минимальные и максимальные значения для числовых полей, или ограничить длину строк.

Пример валидации через Field

class Product(BaseModel):
    price: float = Field(gt=0, description="Цена должна быть больше нуля")
    name: str = Field(min_length=2, max_length=50, description="Название продукта должно быть от 2 до 50 символов")
    

Здесь:

  • Поле price должно быть больше нуля.

  • Поле name имеет ограничения по длине строки: минимум 2 символа, максимум 50.

Вот подробное описание часто используемых встроенных валидаторов:

1. gt, ge, lt, le — для числовых ограничений

Эти валидаторы применяются для ограничения числовых значений (целые числа, числа с плавающей точкой и другие типы, поддерживающие арифметику).

  • gt (greater than): Проверяет, что значение больше указанного числа.
    Пример: Field(gt=0) — значение должно быть больше нуля.

  • ge (greater than or equal): Проверяет, что значение больше или равно указанному числу.
    Пример: Field(ge=1) — значение должно быть не меньше единицы.

  • lt (less than): Проверяет, что значение меньше указанного числа.
    Пример: Field(lt=100) — значение должно быть меньше ста.

  • le (less than or equal): Проверяет, что значение меньше или равно указанному числу.
    Пример: Field(le=10) — значение должно быть не больше десяти.

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

Пример с числовыми ограничениями:

class Product(BaseModel):
    price: float = Field(gt=0, le=10000, description="Цена должна быть положительной и не превышать 10 000")
    rating: int = Field(ge=1, le=5, description="Рейтинг должен быть от 1 до 5")

Здесь:

  • Поле price должно быть больше 0 и не больше 10 000.

  • Поле rating должно находиться в диапазоне от 1 до 5.

2. max_length, min_length — для строковых полей

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

  • min_length: Задаёт минимальное количество символов, которое должно быть в строке.
    Пример: Field(min_length=3) — строка должна быть не короче 3 символов.

  • max_length: Задаёт максимальное количество символов, которое может содержать строка.
    Пример: Field(max_length=100) — строка должна содержать не более 100 символов.

Пример использования валидаторов длины строк:

class User(BaseModel):
    username: str = Field(min_length=3, max_length=20, description="Имя пользователя должно содержать от 3 до 20 символов")
    bio: str = Field(max_length=300, description="Описание профиля не должно превышать 300 символов")

Здесь:

  • Поле username должно содержать от 3 до 20 символов.

  • Поле bio ограничено 300 символами.

3. regex — для проверки по регулярному выражению

Этот валидатор позволяет проверять строковые значения на соответствие регулярному выражению. Регулярные выражения (regex) дают возможность гибко описывать допустимые форматы строк — например, для проверки адресов электронной почты, телефонных номеров, форматов дат и т. д.

  • regex: Задаёт регулярное выражение, которому должна соответствовать строка.Пример: Field(regex=r"[^@]+@[^@]+\.[^@]+") — проверка, что строка является корректным email-адресом.

Пример использования regex:

class User(BaseModel):
    email: str = Field(regex=r"[^@]+@[^@]+\.[^@]+", description="Электронная почта должна быть в корректном формате")
    phone_number: str = Field(regex=r"^\+\d{1,3}\s?\d{4,14}$", description="Номер телефона должен быть в формате +123456789")

Здесь:

  • Поле email должно соответствовать шаблону email-адреса (набор символов до @, доменное имя, точка и домен).

  • Поле phone_number должно соответствовать формату международного телефонного номера.

Динамические значения по умолчанию

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

Пример с default_factory

from uuid import uuid4


class Item(BaseModel):
    id: str = Field(default_factory=lambda: uuid4().hex)

Здесь каждое поле id будет автоматически получать уникальный идентификатор при создании нового объекта модели Item.

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

Для совместимости с внешними API или для удобства чтения кода можно задавать алиасы полей. Алиас — это альтернативное имя для поля, которое будет использоваться при сериализации или десериализации.

Пример с алиасами

class User(BaseModel):
    username: str = Field(alias="user_name")

Когда вы задаёте алиас для поля, это поле будет иметь одно имя в вашем Python-коде, но при отправке данных в формате JSON или чтении из него оно будет использовать другое имя, указанное в качестве алиаса.

В этом примере поле username будет сериализоваться и десериализоваться под именем user_name.

Исключение полей из сериализации

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

Пример исключения полей

class User(BaseModel):
    password: str = Field(exclude=True)

В данном примере поле password будет исключено из сериализованного представления объекта модели.

Это значит, что при сериализации объекта модели (например, при преобразовании его в JSON или словарь), поле password не будет включено в результат. Таким образом, вы можете скрыть конфиденциальную информацию, такую как пароли, от внешних систем или клиентов.

Настройка строкового представления модели

Можно контролировать, какие поля будут отображаться в строковом представлении модели, используя параметр repr.

Пример настройки представления

class Config(BaseModel):
    debug_mode: bool = Field(repr=False)

Строковое представление объекта модели — это то, что возвращается при вызове repr() или str(). Например, когда вы хотите увидеть состояние объекта или отладить код, вы можете вывести его строковое представление.

Параметр repr=False позволяет исключить определённые поля из этого представления, чтобы они не отображались при выводе объекта, даже если они присутствуют в модели.

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

Расширенные возможности через Annotated

Теперь, когда мы разобрались с базовыми возможностями Field, можно перейти к использованию аннотаций через Annotated. Этот способ позволяет добавить метаданные и валидацию более гибко и точно.

Пример использования Annotated

from typing_extensions import Annotated

class User(BaseModel):
    id: Annotated[int, Field(gt=0)]
    name: Annotated[str, Field(min_length=2, max_length=50)]
    email: Annotated[str, Field(regex=r"[^@]+@[^@]+\.[^@]+")]
    role: Annotated[str, Field(default="user")]

Здесь:

  • Поле id должно быть больше нуля.

  • Поле name ограничено длиной от 2 до 50 символов.

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

  • Поле role имеет значение по умолчанию — "user".

В настоящее время наиболее предпочтительным подходом считается использование Annotated в водробном описании полей.

Функция Field в Pydantic 2 предоставляет разработчикам гибкие инструменты для настройки полей моделей. С её помощью можно точно настраивать валидацию, задавать значения по умолчанию, использовать алиасы и добавлять метаданные для документации. А использование Annotated позволяет сделать этот процесс ещё более мощным и удобным.

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

Конфигурация моделей в Pydantic 2

В Pydantic 2 конфигурация моделей теперь задаётся через ConfigDict, а не через старый формат с классом Config. Это важное изменение, которое упрощает настройку и делает её более гибкой.

Как это выглядит теперь:

Вместо того чтобы писать:

class MyModel(BaseModel):
    class Config:
        from_attributes = True

теперь используем ConfigDict:

from pydantic import BaseModel, ConfigDict

class MyModel(BaseModel):
    model_config = ConfigDict(from_attributes=True)

Основные опции ConfigDict

  • from_attributes=True — позволяет создавать объект модели напрямую из атрибутов Python-объектов (например, когда поля модели совпадают с атрибутами другого объекта). Чаще всего опция применяется для преобразования моделей ORM к моделям Pydantic.

  • str_to_lower, str_to_upper - преобразование всех строк модели в нижний или верхний регистр

  • str_strip_whitespace - cледует ли удалять начальные и конечные пробелы для типов str (аналог strip)

  • str_min_length, str_max_length - задает максимальную и минимальную длину строки для всех строковых полей

  • use_enum_values - cледует ли заполнять модели значениями, выбранными из перечислений, вместо того чтобы использовать необработанные значения? Это часто требуется при работе с моделями ORM, в которых колонки определены как перечисления (ENUM).

Почему это важно?

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

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

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

Наследование в Pydantic 2

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

Пример работы наследования в Pydantic:

from pydantic import BaseModel


class ParentModel(BaseModel):
    name: str
    age: int

    
class ChildModel(ParentModel):
    school: str

    
parent = ParentModel(name="Alex", age=40)
child = ChildModel(name="Bob", age=12, school="Greenwood High")

В этом примере ChildModel наследует поля name и age от ParentModel и добавляет свое поле school.

Преимущества:

  • Повторное использование кода: общие поля и методы можно определить в базовой модели.

  • Расширяемость: дочерние модели могут добавлять новые поля или методы.

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

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

Связка Pydantic 2 с ORM на примере SQLAlchemy

Если вы следите за моими публикациями на Хабре, то знаете о серии статей, посвященных работе с Python-фреймворком SQLAlchemy. В предыдущей статье "Асинхронный SQLAlchemy 2: пошаговый гайд по управлению сессиями, добавлению и извлечению данных с Pydantic" мы подробно рассмотрели вопрос интеграции SQLAlchemy с Pydantic

Сегодня я кратко расскажу о ключевых концепциях, объясняющих, почему SQLAlchemy нуждается в Pydantic, и приведу практический пример.

Зачем SQLAlchemy нужен Pydantic?

При работе с SQLAlchemy в ORM-стиле мы сталкиваемся с одним существенным неудобством: данные возвращаются в виде объектов моделей таблиц, что не всегда удобно для дальнейшей обработки. Разработчики обычно предпочитают работать с данными в формате JSON или Python-словарей. Именно здесь на помощь приходит Pydantic.

Принцип интеграции SQLAlchemy и Pydantic

Процесс связывания SQLAlchemy и Pydantic можно описать следующими шагами:

  1. Описание модели таблицы в SQLAlchemy

  2. Создание Pydantic-модели для работы с полученными данными

  3. Запрос данных из таблицы через SQLAlchemy

  4. Преобразование объекта SQLAlchemy в объект Pydantic

  5. Использование методов model_dump или model_dump_json для получения данных в нужном формате

Практический пример

Рассмотрим пример интеграции SQLAlchemy и Pydantic:

from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.orm import declarative_base, sessionmaker
from pydantic import BaseModel, ConfigDict


# Шаг 1: Описание модели SQLAlchemy
Base = declarative_base()


class UserORM(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    name = Column(String)
    email = Column(String)

    
# Шаг 2: Создание Pydantic-модели
class UserPydantic(BaseModel):
    id: int
    name: str
    email: str
    
    model_config = ConfigDict(from_attributes=True)

    
# Настройка базы данных и сессии
engine = create_engine("sqlite:///example.db")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)


# Шаг 3: Запрос данных
def get_user(user_id: int):
    with Session() as session:
        user = session.select(UserORM).filter_by(id = user_id).scalar_one_or_none()
        
        # Шаг 4: Преобразование объекта SQLAlchemy в Pydantic
        if user:
            user_pydantic = UserPydantic.from_orm(user)
            
            # Шаг 5: Получение данных в нужном формате
            return user_pydantic.dict()
        return None

      
# Пример использования
user_data = get_user(1)
print(user_data)

Здесь мы видим использование методов from_orm() для создания Pydantic-модели из ORM-объекта и dict() для преобразования Pydantic-модели в словарь.

Современный подход

С выходом новых версий Pydantic (особенно 2.0+) рекомендуется использовать следующий стиль:

def get_user(user_id: int):
    with Session() as session:
        user = session.select(UserORM).filter_by(id = user_id).scalar_one_or_none()
        
        # Шаг 4: Преобразование объекта SQLAlchemy в Pydantic
        if user:
            user_pydantic = UserPydantic.model_validate(user)
            
            # Шаг 5: Получение данных в нужном формате
            return user_pydantic.model_dump()
        return None

Ключевые изменения и их обоснование

  1. from_orm() → model_validate()

    • from_orm() теперь является алиасом для model_validate().

    • model_validate() более универсален и может использоваться не только с ORM-объектами.

    • Рекомендуется использовать model_validate() для большей гибкости и соответствия современным стандартам Pydantic.

  2. dict() → model_dump()

    • dict() стал алиасом для model_dump().

    • model_dump() предоставляет более широкие возможности настройки вывода.

    • Использование model_dump() делает код более явным и соответствующим новому API Pydantic.

Хотя оба варианта кода функциональны, переход на новые методы (model_validate() и model_dump()) рекомендуется по следующим причинам:

  1. Соответствие современным стандартам и рекомендациям Pydantic.

  2. Улучшенная читаемость кода и явное указание намерений.

  3. Возможность использования дополнительных опций, доступных в новых методах.

  4. Подготовка кода к будущим обновлениям библиотеки.

Использование from_attributes в model_validate

В Pydantic 2.0+ метод model_validate стал более гибким и удобным. Вы можете напрямую указать from_attributes=True при вызове метода:

user_pydantic = UserPydantic.model_validate(user, from_attributes=True)

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

Другие полезные атрибуты model_validate

Помимо from_attributes, метод model_validate имеет несколько других полезных атрибутов:

  1. strict: bool | None

    • Когда установлен в True, применяет строгую валидацию типов.

    • Пример: model_validate(data, strict=True)

  2. context: Any | None

    • Позволяет передать дополнительный контекст валидаторам.

    • Пример: model_validate(data, context={'user_id': 123})

  3. from_attributes: bool | None

    • Как мы уже обсудили, позволяет извлекать данные из атрибутов объекта.

Пример использования

class User:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

        
class UserModel(BaseModel):
    name: str
    age: int

    
user = User("Alice", 30)


# Использование from_attributes
user_model = UserModel.model_validate(user, from_attributes=True)


# Использование strict и context
data = {"name": "Bob", "age": "25"}
user_model = UserModel.model_validate(
    data,
    strict=True,
    context={"source": "external_api"},
    from_attributes=False
)

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

Использование from_attributes непосредственно в model_validate особенно удобно, когда вам нужно динамически переключаться между разными источниками данных без изменения конфигурации модели.

Концепция обратной валидации

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

Пример реализации

1. Определение Pydantic модели для фильтров

from pydantic import BaseModel, ConfigDict


class TelegramIDModel(BaseModel):
    telegram_id: int

    model_config = ConfigDict(from_attributes=True)

2. Метод для поиска в базе данных

from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from sqlalchemy.exc import SQLAlchemyError


class BaseRepository:
    @classmethod
    async def find_one_or_none(cls, session: AsyncSession, filters: BaseModel):
        filter_dict = filters.model_dump(exclude_unset=True)
        try:
            query = select(cls.model).filter_by(**filter_dict)
            result = await session.execute(query)
            return result.scalar_one_or_none()
        except SQLAlchemyError as e:
            # Обработка ошибок
            raise

3. Использование

async def get_user_by_telegram_id(session: AsyncSession, telegram_id: int):
    filters = TelegramIDModel(telegram_id=telegram_id)
    user = await UserRepository.find_one_or_none(session, filters)
    return user

Кстати, в своей универсальной заготовке для создания Telegram-ботов на основе Aiogram 3 и SQLAlchemy я использовал более сложный пример. Если вас интересует исходный код этой заготовки и другой эксклюзивный контент, который я не публикую на Хабре, вы можете получить его бесплатно в моём телеграм-канале «Легкий путь в Python».

Преимущества этого подхода

  1. Типобезопасность: Pydantic обеспечивает валидацию типов входных данных.

  2. Гибкость: Легко создавать различные модели фильтров для разных запросов.

  3. Читаемость: Код становится более понятным и структурированным.

  4. Переиспользование: Модели фильтров можно использовать в разных частях приложения.

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

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

Практическое применение Pydantic в различных областях

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

1. Веб-разработка

Pydantic часто используется в веб-фреймворках, таких как FastAPI и Flask, для валидации входящих данных от пользователей. Это позволяет разработчикам легко обрабатывать JSON-запросы и гарантировать, что данные соответствуют ожидаемым типам.

На Хабре я опубликовал более десяти крупных статей о разработке собственного API с использованием FastApi. Если вас интересует эта тема, рекомендую заглянуть в мой профиль. Там вы найдёте множество примеров того, как использовать FastApi вместе с Pydantic.

2. API и микросервисы

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

3. Обработка конфигураций

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

4. Обработка данных

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

5. Тестирование

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

Заключение

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

  1. Мы разобрали основы Pydantic и его важность в экосистеме Python.

  2. Изучили ключевые концепции Pydantic 2: модели, поля, валидацию, ConfigDict и наследование.

  3. Рассмотрели интеграцию Pydantic с ORM, в частности с SQLAlchemy.

  4. Обсудили практическое применение Pydantic в различных областях разработки.

  5. Отметили улучшения в производительности и функциональности Pydantic 2.

Помните, что для полного освоения Pydantic 2 необходима практика. Экспериментируйте с кодом и интегрируйте Pydantic в ваши проекты.

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

Для получения ещё большего количества эксклюзивного контента, присоединяйтесь к моему Telegram-каналу "Легкий путь в Python".

Спасибо за ваше время! Удачи в ваших проектах, и пусть Pydantic 2 станет вашим надежным помощником в мире Python-разработки!

Комментарии (0)