Какие архитектурные решения Pydantic навязывает команде?
Pydantic навязывает schema-first подход: модели становятся единственным источником правды для валидации, сериализации и документации. Это тянет за собой разделение DTO и доменных объектов, явное объявление контрактов на границах слоёв.
Schema-first как основное архитектурное решение
Когда команда принимает Pydantic, она фактически принимает schema-first архитектуру: каждый публичный интерфейс описывается через BaseModel, и эта модель становится одновременно валидатором, сериализатором и документацией. Это меняет несколько архитектурных решений.
Разделение слоёв: DTO vs доменные объекты
Pydantic хорошо работает как DTO (Data Transfer Object) на границах сервиса, но плохо — как доменный объект с поведением. Команды, не знающие этого, кладут бизнес-логику в @field_validator или model_post_init, что приводит к жирным моделям и сложной тестируемости.
from pydantic import BaseModel, model_validator
from typing import Self
# DTO — граница HTTP
class CreateOrderRequest(BaseModel):
user_id: int
items: list[int]
promo_code: str | None = None
@model_validator(mode="after")
def items_not_empty(self) -> Self:
if not self.items:
raise ValueError("items list cannot be empty")
return self
# Доменный объект — бизнес-логика вне Pydantic
class Order:
def __init__(self, user_id: int, items: list[int]) -> None:
self.user_id = user_id
self.items = items
def apply_promo(self, code: str) -> None:
# бизнес-правила здесь, не в Pydantic
...
Явные контракты на каждой границе
Pydantic вынуждает команду объявлять входные и выходные типы явно. В FastAPI это выражается через отдельные модели для запросов и ответов:
from pydantic import BaseModel
from datetime import datetime
class JobCreate(BaseModel): # входной контракт
title: str
company_id: int
class JobResponse(BaseModel): # выходной контракт
id: int
title: str
created_at: datetime
model_config = {"from_attributes": True} # из ORM-объекта
Это решение: не переиспользовать одну модель для создания и чтения — иначе в ответе окажутся поля, которые клиент не должен видеть.
Конфигурация через BaseSettings
Pydantic навязывает централизованную конфигурацию: все переменные окружения собираются в одном месте, что упрощает тестирование через model_config = {"env_file": ".env.test"}.
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
db_url: str
redis_url: str = "redis://localhost:6379"
jwt_secret: str
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
# Singleton через lru_cache
from functools import lru_cache
@lru_cache
def get_settings() -> Settings:
return Settings()
Подводные камни
- Смешение DTO и доменных объектов: логика в
@field_validatorпревращает модель в God Object. - Мутабельные модели по умолчанию —
model_config = {"frozen": True}нужно включать явно для value objects. - Pydantic v2 изменил API:
@validator→@field_validator,.dict()→.model_dump(). Миксование v1/v2 кода в одном проекте — частая ошибка при обновлении. - Circular references между моделями требуют
model_rebuild()— забытый вызов даётPydanticUserErrorв runtime. from_attributes=Trueвmodel_configнужен для интеграции с SQLAlchemy ORM, без негоmodel_validate(orm_obj)бросает исключение.- Наследование моделей может нарушить JSON Schema: поля родителя попадают в схему дочернего класса не всегда предсказуемо.
- Тяжёлые
model_validator(mode="before")блокируют весь разбор — используйте только там, где действительно нужна предобработка. - Генерация схемы через
model_json_schema()не учитываетresponse_model_excludeFastAPI — тестируйте OpenAPI отдельно.
What hurts your answer
- Знать термины Pydantic, но не понимать связи между абстракциями
- Объяснять поведение через отдельные примеры вместо причинной модели
- Не связывать mental model с диагностикой ошибок
What they're listening for
- Понимает ключевые абстракции Pydantic
- Может предсказывать поведение системы через mental model
- Связывает модель с debugging и production decisions