← Назад к курсу

Учебное пособие по работе с Python Pydantic

1. Назначение и введение

1.1 Что такое Pydantic?

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

  • Валидации данных — автоматической проверки соответствия данных заданным типам и правилам
  • Парсинга и сериализации — преобразования данных между различными форматами (JSON, dict, Python-объекты)
  • Управления настройками — работы с конфигурациями приложений

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

В реальных проектах данные часто приходят из внешних источников — API, пользовательского ввода, файлов конфигурации. Без надёжной валидации это приводит к:

  • Ошибкам типов во время выполнения
  • Разрастанию проверочного кода (if-ы, try-except) по всему проекту
  • Смешиванию бизнес-логики с проверочной логикой

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

1.3 Ключевые преимущества

Преимущество Описание
Работает на type hints Валидация управляется аннотациями типов — меньше кода, лучше интеграция с IDE
Высокая скорость Ядро валидации написано на Rust
Автоматическое приведение типов Строка "25" автоматически становится int 25
Понятные ошибки Вместо cryptic-исключений — чёткие сообщения о том, что пошло не так
Генерация JSON Schema Модели могут экспортировать JSON-схему для интеграции с другими инструментами
Огромная экосистема ~8000 пакетов на PyPI используют Pydantic: FastAPI, LangChain, SQLModel, HuggingFace и другие

1.4 Установка

pip install pydantic

Или через conda:

conda install pydantic -c conda-forge

2. Теория: основные концепции

2.1 BaseModel — основа всего

Центральная концепция Pydantic — модель, создаваемая наследованием от BaseModel. Поля модели объявляются с помощью аннотаций типов:

from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int
    email: str

Важные правила:

  • Поля без значения по умолчанию — обязательные
  • Поля с = None или = значение — опциональные
  • Pydantic автоматически проверяет аннотации при создании класса и строит схему валидации

2.2 Типы данных

Pydantic поддерживает все стандартные типы Python, а также добавляет свои:

from pydantic import BaseModel
from typing import Optional, List, Dict
from datetime import datetime

class Example(BaseModel):
    # Стандартные типы
    id: int
    name: str
    price: float
    is_active: bool
    
    # Контейнеры
    tags: List[str]
    metadata: Dict[str, str]
    
    # Опциональные поля
    description: Optional[str] = None
    
    # Дата и время
    created_at: datetime

Pydantic умеет автоматически преобразовывать строки в datetime, числа в строки и т.д..

2.3 Field — расширенные ограничения

Когда простой аннотации типа недостаточно, используется Field для задания дополнительных ограничений:

from pydantic import BaseModel, Field
from typing import Optional

class Product(BaseModel):
    name: str = Field(..., min_length=3, max_length=100, description="Название товара")
    price: float = Field(gt=0, le=9999.99, description="Цена должна быть положительной")
    quantity: int = Field(ge=0, le=1000, description="Количество на складе")
    sku: Optional[str] = Field(None, pattern=r'^[A-Z]{2}-\d{4}$', description="Артикул")

Основные параметры Field:

Параметр Назначение
... (Ellipsis) Поле обязательно (не может быть None)
default Значение по умолчанию
min_length / max_length Минимальная/максимальная длина строки
gt / ge / lt / le Больше/больше или равно/меньше/меньше или равно
pattern Регулярное выражение для строки
description Описание поля (для документации)
alias Альтернативное имя поля

2.4 Валидация при создании экземпляра

# Корректные данные — создание успешно
user = User(name="Alice", age=25, email="alice@example.com")

# Автоматическое приведение типов
user = User(name="Bob", age="30", email="bob@example.com")
print(type(user.age))  # <class 'int'>

# Некорректные данные — ValidationError
from pydantic import ValidationError

try:
    user = User(name="A", age="abc", email="not-an-email")
except ValidationError as e:
    print(e.json())  # Подробная информация об ошибках

3. Практические примеры

3.1 Пример 1: Модель пользователя с валидацией

from pydantic import BaseModel, Field, ValidationError, EmailStr
from typing import Optional

class User(BaseModel):
    username: str = Field(..., min_length=3, max_length=20, description="Имя пользователя")
    email: EmailStr = Field(..., description="Email-адрес")
    age: int = Field(gt=0, lt=150, description="Возраст (1-149 лет)")
    is_active: bool = True
    phone: Optional[str] = Field(None, pattern=r'^\+?\d{10,15}$')

# Валидные данные
user = User(
    username="alex_dev",
    email="alex@example.com",
    age=28
)
print(user.model_dump())
# {'username': 'alex_dev', 'email': 'alex@example.com', 'age': 28, 'is_active': True, 'phone': None}

# Невалидные данные
try:
    user = User(
        username="a",  # Слишком короткое
        email="invalid",  # Невалидный email
        age=200  # Слишком большой возраст
    )
except ValidationError as e:
    print(e)

3.2 Пример 2: Работа с JSON

Pydantic отлично работает с JSON-данными — например, из API или файлов:

import json
from pydantic import BaseModel

class Address(BaseModel):
    street: str
    city: str
    zip_code: str

class Person(BaseModel):
    name: str
    age: int
    address: Address

# Парсинг из JSON-строки
json_data = '''
{
    "name": "John Doe",
    "age": 30,
    "address": {
        "street": "123 Main St",
        "city": "Boston",
        "zip_code": "02101"
    }
}
'''

# Метод model_validate_json для парсинга JSON
person = Person.model_validate_json(json_data)
print(person.name)  # John Doe
print(person.address.city)  # Boston

# Сериализация в JSON
print(person.model_dump_json(indent=2))

3.3 Пример 3: Опциональные поля и значения по умолчанию

В реальных данных часто встречаются пропущенные поля:

from pydantic import BaseModel
from typing import Optional

class Product(BaseModel):
    name: str  # Обязательное поле
    price: float  # Обязательное поле
    description: Optional[str] = None  # Опциональное
    discount: float = 0.0  # Значение по умолчанию
    in_stock: bool = True  # Значение по умолчанию

# Можно не указывать опциональные поля
product = Product(name="Laptop", price=999.99)
print(product.discount)  # 0.0
print(product.in_stock)  # True
print(product.description)  # None

3.4 Пример 4: Вложенные модели

Pydantic поддерживает вложенные структуры любой глубины:

from pydantic import BaseModel
from typing import List

class Item(BaseModel):
    name: str
    price: float
    quantity: int = 1

class Address(BaseModel):
    street: str
    city: str
    country: str = "US"

class Order(BaseModel):
    order_id: str
    customer_name: str
    shipping_address: Address  # Вложенная модель
    items: List[Item]  # Список вложенных моделей
    total: float

data = {
    "order_id": "ORD-001",
    "customer_name": "Alice",
    "shipping_address": {
        "street": "456 Oak Ave",
        "city": "Chicago"
    },
    "items": [
        {"name": "Book", "price": 19.99, "quantity": 2},
        {"name": "Pen", "price": 2.50, "quantity": 5}
    ],
    "total": 52.48
}

order = Order(**data)
print(order.items[0].name)  # Book
print(order.shipping_address.country)  # US

3.5 Пример 5: Пользовательские валидаторы

Для сложной логики валидации используются декораторы @field_validator и @model_validator:

from pydantic import BaseModel, field_validator, model_validator, ValidationError

class PasswordUser(BaseModel):
    username: str
    password: str
    confirm_password: str
    age: int

    # Валидатор отдельного поля
    @field_validator('password')
    @classmethod
    def validate_password(cls, v: str) -> str:
        if len(v) < 8:
            raise ValueError('Пароль должен содержать минимум 8 символов')
        if not any(c.isdigit() for c in v):
            raise ValueError('Пароль должен содержать хотя бы одну цифру')
        if not any(c.isupper() for c in v):
            raise ValueError('Пароль должен содержать хотя бы одну заглавную букву')
        return v

    @field_validator('age')
    @classmethod
    def validate_age(cls, v: int) -> int:
        if v < 18:
            raise ValueError('Возраст должен быть не менее 18 лет')
        return v

    # Валидатор всей модели (между полями)
    @model_validator(mode='after')
    def check_passwords_match(self) -> 'PasswordUser':
        if self.password != self.confirm_password:
            raise ValueError('Пароли не совпадают')
        return self

# Проверка
try:
    user = PasswordUser(
        username="alex",
        password="weak",
        confirm_password="weak",
        age=20
    )
except ValidationError as e:
    print(e)
    # Пароль должен содержать минимум 8 символов

4. Продвинутые возможности

4.1 Strict Mode (Строгий режим)

По умолчанию Pydantic работает в свободном режиме — пытается автоматически приводить типы. Например, строка "25" становится числом 25.

В строгом режиме Pydantic требует точного соответствия типов:

from pydantic import BaseModel, ConfigDict

class StrictUser(BaseModel):
    model_config = ConfigDict(strict=True)
    
    id: int
    name: str

# В строгом режиме это вызовет ошибку
try:
    user = StrictUser(id="123", name="Alice")  # Ошибка: ожидается int
except ValidationError as e:
    print(e)

4.2 Alias (Псевдонимы полей)

Псевдонимы полезны при работе с API, где имена полей отличаются от Python-стиля:

from pydantic import BaseModel, Field

class ApiUser(BaseModel):
    user_id: int = Field(alias="userId")
    first_name: str = Field(alias="firstName")
    last_name: str = Field(alias="lastName")

data = {
    "userId": 123,
    "firstName": "John",
    "lastName": "Doe"
}

# model_validate с by_alias=True для работы с алиасами
user = ApiUser.model_validate(data)
print(user.user_id)  # 123
print(user.model_dump(by_alias=True))  # {'userId': 123, 'firstName': 'John', 'lastName': 'Doe'}

4.3 TypeAdapter для ad-hoc валидации

TypeAdapter позволяет применять логику валидации Pydantic к произвольным типам, не создавая модель:

from pydantic import TypeAdapter
from typing import List

# Валидация списка чисел
adapter = TypeAdapter(List[int])
data = adapter.validate_python(["1", "2", "3"])
print(data)  # [1, 2, 3]

# Валидация JSON
json_str = '["10", "20", "30"]'
result = adapter.validate_json(json_str)
print(result)  # [10, 20, 30]

5. Миграция с Pydantic V1 на V2

Pydantic V2 — это текущая production-версия с существенными улучшениями и некоторыми breaking changes.

5.1 Ключевые изменения

V1 V2
parse_obj() model_validate()
parse_raw() model_validate_json()
dict() model_dump()
json() model_dump_json()
@validator @field_validator
@root_validator @model_validator

5.2 Пример миграции

V1:

from pydantic import BaseModel, validator

class User(BaseModel):
    name: str
    age: int
    
    @validator('age')
    def check_age(cls, v):
        if v < 0:
            raise ValueError('Age must be positive')
        return v

user = User.parse_obj({"name": "Alice", "age": 25})
print(user.json())

V2:

from pydantic import BaseModel, field_validator

class User(BaseModel):
    name: str
    age: int
    
    @field_validator('age')
    @classmethod
    def check_age(cls, v: int) -> int:
        if v < 0:
            raise ValueError('Age must be positive')
        return v

user = User.model_validate({"name": "Alice", "age": 25})
print(user.model_dump_json())

5.3 Инструмент для автоматической миграции

Для автоматического перевода кода с V1 на V2 существует утилита bump-pydantic:

pip install bump-pydantic
cd /path/to/your/project
bump-pydantic my_package

5.4 Совместное использование V1 и V2

Если нужно временно использовать V1 в проекте с V2:

# Использование V1 API в проекте с V2
from pydantic.v1 import BaseModel, validator

class User(BaseModel):
    name: str

6. Заключение и лучшие практики

6.1 Когда использовать Pydantic

  • Веб-приложения (FastAPI, Django Ninja) — валидация запросов и ответов
  • Работа с API — парсинг и валидация внешних данных
  • Конфигурация приложений — управление настройками из env-файлов
  • Обработка пользовательского ввода — формы, JSON-данные
  • Проекты с LLM — LangChain и другие AI-инструменты

6.2 Лучшие практики

  1. Держите модели в отдельном модуле (models.py или schemas/)
  2. Используйте Field для документации — параметр description помогает генерировать качественную документацию
  3. Не злоупотребляйте сложными валидаторами — выносите сложную логику в отдельные функции
  4. Используйте model_dump() вместо dict() в Pydantic V2
  5. При работе с API используйте алиасы (alias) для соответствия стилю API

6.3 Пример хорошо структурированного проекта

project/
├── src/
│   ├── models/
│   │   ├── __init__.py
│   │   ├── user.py      # Модели пользователей
│   │   ├── product.py   # Модели товаров
│   │   └── order.py     # Модели заказов
│   ├── services/        # Бизнес-логика
│   └── main.py
└── tests/
    └── test_models.py   # Тесты моделей

6.4 Резюме

Pydantic — это must-have инструмент для любого Python-разработчика, работающего с данными. Он:

  • Устраняет необходимость в ручной валидации
  • Делает код чище и самодокументируемым
  • Предоставляет понятные сообщения об ошибках
  • Интегрируется с экосистемой Python (FastAPI, LangChain и др.)

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