Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Продвинутый Pydantic
pydantic_advanced

Продвинутый Pydantic

Валидаторы, сериализация, сложные типы данных

Открыть лабораториюv1.0Запускается локально из публичного репозитория

Продвинутый Pydantic

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

#Полевые валидаторы (Field Validators)

Валидаторы позволяют проверять и преобразовывать значения полей:

from pydantic import BaseModel, field_validator, EmailStr from datetime import datetime class UserCreate(BaseModel): username: str email: EmailStr password: str birth_date: datetime | None = None @field_validator('username') @classmethod def validate_username(cls, v: str) -> str: if not v.isalnum(): raise ValueError('Username must be alphanumeric') if len(v) < 3: raise ValueError('Username must be at least 3 characters') return v.lower() # Преобразуем к нижнему регистру @field_validator('password') @classmethod def validate_password(cls, v: str) -> str: if len(v) < 8: raise ValueError('Password must be at least 8 characters') if not any(c.isupper() for c in v): raise ValueError('Password must contain uppercase letter') if not any(c.isdigit() for c in v): raise ValueError('Password must contain digit') return v

#Валидация нескольких полей

class UserCreate(BaseModel): password: str password_confirm: str @field_validator('password_confirm') @classmethod def passwords_match(cls, v: str, info) -> str: if 'password' in info.data and v != info.data['password']: raise ValueError('Passwords do not match') return v

info.data содержит уже валидированные поля.

#Валидатор модели (Model Validator)

Для валидации, затрагивающей несколько полей:

from pydantic import BaseModel, model_validator, Field class Event(BaseModel): title: str start_date: datetime end_date: datetime @model_validator(mode='after') def check_dates(self) -> 'Event': if self.end_date < self.start_date: raise ValueError('end_date must be after start_date') return self

mode='after' — валидация после создания модели.

#Вычисляемые поля

from pydantic import BaseModel, computed_field class Product(BaseModel): name: str price: float quantity: int @computed_field @property def total_value(self) -> float: return self.price * self.quantity @computed_field @property def in_stock(self) -> bool: return self.quantity > 0

Пример ответа:

{ "name": "Laptop", "price": 999.99, "quantity": 5, "total_value": 4999.95, "in_stock": true }

#Сериализация

#model_dump() и model_dump_json()

class User(BaseModel): id: int username: str password: str created_at: datetime user = User(id=1, username='john', password='secret', created_at=datetime.now()) # В словарь data = user.model_dump() # В JSON-строку json_str = user.model_dump_json() # Исключить поля data = user.model_dump(exclude={'password'}) # Исключить None data = user.model_dump(exclude_none=True) # Только определённые поля data = user.model_dump(include={'id', 'username'})

#Конфигурация сериализации

from pydantic import BaseModel, ConfigDict from datetime import datetime class Event(BaseModel): model_config = ConfigDict( json_schema_extra={ 'example': { 'title': 'Conference', 'start_date': '2024-01-01T09:00:00' } } ) title: str start_date: datetime

#Сложные типы данных

#Union — одно из нескольких значений

from typing import Union class Response(BaseModel): status: str data: Union[str, int, dict] # Может быть строкой, числом или словарём

В Python 3.10+:

class Response(BaseModel): status: str data: str | int | dict

#Literal — фиксированные значения

from typing import Literal class Post(BaseModel): title: str status: Literal['draft', 'published', 'archived'] priority: Literal[1, 2, 3, 4, 5] = 3

#Generic-модели

from typing import Generic, TypeVar from pydantic import BaseModel T = TypeVar('T') class APIResponse(BaseModel, Generic[T]): success: bool data: T message: str | None = None # Использование class User(BaseModel): id: int username: str response = APIResponse[User]( success=True, data={'id': 1, 'username': 'john'}, message='User retrieved' )

#Вложенные generics

class PaginatedResponse(BaseModel, Generic[T]): items: list[T] total: int page: int page_size: int # Использование response = PaginatedResponse[User]( items=[{'id': 1, 'username': 'john'}], total=100, page=1, page_size=10 )

#Кастомные типы

#Создание своего типа (правильный способ)

В Pydantic v2 кастомные типы создаются через Annotated и AfterValidator:

from pydantic import BaseModel, AfterValidator from typing import Annotated from pydantic_core import PydanticCustomError import re def validate_phone(v: str) -> str: pattern = r'^\+?1?\d{9,15}$' if not re.match(pattern, v): raise PydanticCustomError( 'phone_format', 'Invalid phone number format' ) return v PhoneNumber = Annotated[str, AfterValidator(validate_phone)] class User(BaseModel): phone: PhoneNumber # Примеры User(phone='+1234567890') # ✓ Пройдёт User(phone='invalid') # ✗ Ошибка: Invalid phone number format

Почему не через класс? В Pydantic v1 можно было определить класс с методом validate(), но в v2 этот подход не работает — метод validate просто не вызывается. Используйте Annotated + AfterValidator.

#EmailStr и другие встроенные типы

from pydantic import BaseModel, EmailStr, HttpUrl, AnyUrl, IPvAnyAddress class Network(BaseModel): email: EmailStr # name@example.com website: HttpUrl # https://example.com link: AnyUrl # Любая URL ip: IPvAnyAddress # 192.168.1.1 или ::1

#Валидация с контекстом

from pydantic import BaseModel, field_validator, ValidationInfo class User(BaseModel): username: str role: str @field_validator('role') @classmethod def validate_role(cls, v: str, info: ValidationInfo) -> str: # Доступ к другим полям username = info.data.get('username') if username == 'admin' and v != 'admin': raise ValueError('Admin user must have admin role') return v

#Пример: Полноценная модель с валидацией

from pydantic import ( BaseModel, Field, field_validator, model_validator, computed_field, ConfigDict, EmailStr, HttpUrl ) from datetime import datetime from typing import Literal import re class UserProfile(BaseModel): model_config = ConfigDict( json_schema_extra={ 'example': { 'username': 'john_doe', 'email': 'john@example.com', 'bio': 'Software developer', 'website': 'https://johndoe.com' } } ) username: str = Field( ..., min_length=3, max_length=50, pattern=r'^[a-zA-Z0-9_]+$', description="Unique username" ) email: EmailStr bio: str | None = Field(None, max_length=500) website: HttpUrl | None = None age: int = Field(..., ge=18, le=120) role: Literal['user', 'moderator', 'admin'] = 'user' @field_validator('username') @classmethod def username_not_reserved(cls, v: str) -> str: reserved = ['admin', 'moderator', 'system', 'root'] if v.lower() in reserved: raise ValueError('This username is reserved') return v @field_validator('bio') @classmethod def validate_bio(cls, v: str | None) -> str | None: if v and len(v.strip()) < 10: raise ValueError('Bio must be at least 10 characters') return v @model_validator(mode='after') def check_admin_age(self) -> 'UserProfile': if self.role == 'admin' and self.age < 21: raise ValueError('Admin must be at least 21 years old') return self @computed_field @property def is_adult(self) -> bool: return self.age >= 18 @computed_field @property def profile_url(self) -> str: return f'/users/{self.username}'

#Миграция с Pydantic v1 на v2

#Изменения в синтаксисе

Pydantic v1Pydantic v2
validatorfield_validator
root_validatormodel_validator
@validator('field')@field_validator('field') @classmethod
.dict().model_dump()
.json().model_dump_json()
.parse_obj().model_validate()
class Configmodel_config = ConfigDict()
orm_mode = Truefrom_attributes = True

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

v1:

class User(BaseModel): username: str class Config: orm_mode = True @validator('username') def validate_username(cls, v): return v.lower()

v2:

from pydantic import BaseModel, field_validator, ConfigDict class User(BaseModel): model_config = ConfigDict(from_attributes=True) username: str @field_validator('username') @classmethod def validate_username(cls, v: str) -> str: return v.lower()

#Частые ошибки

#Ошибка 1: Забыли @classmethod

class User(BaseModel): @field_validator('username') def validate_username(cls, v): # Ошибка: нет @classmethod return v

Решение:

@field_validator('username') @classmethod def validate_username(cls, v): return v

#Ошибка 2: Изменение модели в валидаторе

@field_validator('password') @classmethod def hash_password(cls, v: str) -> str: return hash(v) # Ошибка: хешируем при валидации!

Проблема: Пароль хешируется при каждой валидации, а не только при сохранении.

Решение: Хешируйте в CRUD-слое, не в модели.

#Ошибка 3: Использование mode='before' вместо mode='after'

@model_validator(mode='before') def check_all(cls, values: dict) -> dict: # values — это сырой словарь ДО валидации # Поля ещё НЕ приведены к нужным типам! print(values['start_date']) # Может быть строка, а не datetime return values

Проблема: mode='before' получает сырые данные до валидации. Типы могут быть неверны, строки не преобразованы в datetime/int и т.д.

Решение: Для кросс-полевой валидации используйте mode='after' — к этому моменту все поля уже валидированы и приведены к нужным типам:

@model_validator(mode='after') def check_dates(self) -> 'Event': # self.start_date и self.end_date — уже datetime if self.end_date < self.start_date: raise ValueError('end_date must be after start_date') return self

#Ошибка 4: Пытаются получить info.data в поле без контекста

@field_validator('role') @classmethod def validate_role(cls, v: str) -> str: # Ошибка: нет параметра info, нельзя получить другие поля return v

Решение: Добавьте info: ValidationInfo, если нужны другие поля:

@field_validator('role') @classmethod def validate_role(cls, v: str, info: ValidationInfo) -> str: username = info.data.get('username') if username == 'admin' and v != 'admin': raise ValueError('Admin user must have admin role') return v

#Исполняемая лаборатория

Лаборатория «@field_validator, @model_validator, ConfigDict» доступна в публичном GitLab. Она проверяет @field_validator с @classmethod, @model_validator(mode='after'), computed_field, ConfigDict(from_attributes=True), Annotated + AfterValidator для кастомных типов и model_dump(exclude=).

Склонируйте репозиторий и запустите тесты из его корня:

git clone --branch v1.0 --depth 1 https://gitlab.potapov.me/courses/fastapi_pro.git cd fastapi_pro uv sync --group test uv run --group test pytest pydantic_advanced/tests

Далее: Аутентификация JWT