PydanticMiddleExperience

Какие ошибки делают команды при внедрении Pydantic?

Частые ошибки: использование одной модели для input и output (утечка внутренних полей), бизнес-логика в field_validator вместо сервисного слоя, забытый model_rebuild() при circular references, игнорирование ValidationError в продакшне.

Ошибка 1: одна модель для input и output

Команды часто используют одну модель и для приёма запроса, и для ответа. В результате в API утекают внутренние поля (хэш пароля, внутренние флаги).

# Плохо: одна модель
class User(BaseModel):
    id: int
    email: str
    password_hash: str  # утечёт в ответе!

# Хорошо: разделить
class UserCreate(BaseModel):
    email: str
    password: str

class UserResponse(BaseModel):
    id: int
    email: str
    model_config = {"from_attributes": True}

Ошибка 2: бизнес-логика в field_validator

Проверка уникальности email, баланса счёта или наличия записи в БД не должна быть в @field_validator. Pydantic вызывает валидаторы в конструкторе — до того, как у вас есть сессия БД или контекст запроса.

# Плохо: запрос к БД в валидаторе
class UserCreate(BaseModel):
    email: str

    @field_validator("email")
    @classmethod
    def email_unique(cls, v: str) -> str:
        # НЕЛЬЗЯ: нет db session, нет async context
        if db.query(User).filter_by(email=v).first():
            raise ValueError("email already taken")
        return v

# Хорошо: проверка в сервисном слое
async def create_user(data: UserCreate, db: AsyncSession) -> User:
    existing = await db.scalar(select(User).where(User.email == data.email))
    if existing:
        raise HTTPException(status_code=409, detail="email already taken")
    ...

Ошибка 3: игнорирование ValidationError в продакшне

При парсинге внешних данных (Telegram, webhooks) ValidationError содержит точный путь к сломанному полю. Команды ловят его как Exception и теряют контекст.

from pydantic import ValidationError
import logging

logger = logging.getLogger(__name__)

try:
    job = JobCreate.model_validate(raw_data)
except ValidationError as e:
    # e.errors() — список с loc, msg, type
    logger.warning("invalid job payload", extra={"errors": e.errors()})
    raise

Ошибка 4: забытый model_rebuild() при forward references

При circular references между моделями Pydantic v2 требует явного вызова model_rebuild(). Без него — PydanticUserError: "Model is not fully defined" в runtime.

from __future__ import annotations
from pydantic import BaseModel

class Company(BaseModel):
    name: str
    jobs: list[Job] = []

class Job(BaseModel):
    title: str
    company: Company | None = None

# Обязательно после объявления обоих классов:
Company.model_rebuild()
Job.model_rebuild()

Ошибка 5: использование model_config без понимания влияния

Флаги вроде arbitrary_types_allowed=True или populate_by_name=True меняют поведение всей модели. Команды копируют их из SO без понимания последствий.

# arbitrary_types_allowed отключает проверку типов для кастомных классов
class Config(BaseModel):
    model_config = {"arbitrary_types_allowed": True}
    conn: SomeDatabaseConnection  # Pydantic не проверит этот тип

Ошибка 6: мутабельные модели как value objects

По умолчанию Pydantic-модели мутабельны. Для ключей кэша, идентификаторов и других value objects нужно frozen=True.

class JobID(BaseModel):
    model_config = {"frozen": True}
    value: int

# Теперь можно использовать как ключ словаря
cache: dict[JobID, str] = {}

Подводные камни

  • Миграция v1 → v2: @validator без @classmethod не работает в v2 — ошибка тихая в некоторых версиях.
  • .dict() удалён в v2 — используйте .model_dump(). Старый код падает с AttributeError.
  • Алиасы через Field(alias="camelCase") ломают model_dump() без by_alias=True.
  • Тип list[BaseModel] в поле копирует объекты при валидации — неожиданное потребление памяти на больших списках.
  • model_validate с strict=True не приводит "123" к int — поведение отличается от дефолтного режима.
  • Вложенные модели сериализуются рекурсивно — глубокие структуры могут давать RecursionError.
  • Использование Optional[str] вместо str | None в v2 работает, но считается устаревшим стилем.
  • Pydantic не логирует предупреждения при игнорировании лишних полей (default) — используйте model_config = {"extra": "forbid"} на входных моделях.

What hurts your answer

  • Перечислять ошибки без объяснения причин
  • Не отличать beginner mistakes от production failure modes
  • Не предлагать процесс, который предотвращает повторение ошибок

What they're listening for

  • Знает типичные ошибки при работе с Pydantic
  • Понимает причины ошибок
  • Предлагает практики prevention и early detection

Related topics