Kubernetes liveness vs readiness probes: в чём разница и как настроить правильно
Pydantic v2 — это не косметический апдейт, а переписывание ядра на Rust. Валидация стала в 5–50 раз быстрее, API почистили, а старые паттерны из v1 местами сломались. Разберём, что изменилось, как мигрировать и где новые грабли.
Главные изменения в Pydantic v2
Ядро валидации переехало в отдельный крейт pydantic-core, написанный на Rust. Python-слой теперь тонкая обёртка. Что это даёт на практике:
- Скорость: парсинг JSON и валидация моделей — в 5–50x быстрее в зависимости от сложности схемы.
- ConfigDict вместо class Config: вложенный класс
Configзаменён наmodel_config = ConfigDict(...). - model_validator вместо root_validator: новый декоратор с явным режимом
mode="before"/mode="wrap"/mode="after". - field_validator вместо validator: декоратор
@validatorудалён, теперь@field_validatorс явным указанием полей.
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
class User(BaseModel):
model_config = ConfigDict(strict=True, frozen=True)
name: str
email: str
age: int
@field_validator("email")
@classmethod
def validate_email(cls, v: str) -> str:
if "@" not in v:
raise ValueError("Invalid email")
return v.lower()
@model_validator(mode="after")
def check_age_name(self) -> "User":
if self.age < 18 and "admin" in self.name.lower():
raise ValueError("Admins must be 18+")
return self
Бенчмарк: v1 vs v2 на реальных моделях
Тестируем на типичной модели из продакшена — вложенные объекты, списки, Optional-поля, кастомные валидаторы. Окружение: Python 3.11, 1 млн итераций:
# Модель: Order с вложенными Item (5 полей), список из 10 элементов
# Pydantic v1.10.13
# model(**data): 4.2 сек / 1M
# .json() сериализация: 3.8 сек / 1M
# Pydantic v2.6
# model_validate(data): 0.31 сек / 1M (x13.5 быстрее)
# .model_dump_json(): 0.19 сек / 1M (x20 быстрее)
Самый большой прирост — на сериализации в JSON. Rust-ядро сериализует напрямую, минуя промежуточный dict. На простых flat-моделях разница скромнее (x5–x7), на глубоко вложенных — до x50.
Пошаговая миграция с v1 на v2
Миграция большого проекта — не «поменять импорты». Вот проверенный план:
- Запустите bump-pydantic: автоматический кодмод, который переименовывает 80% изменений.
pip install bump-pydantic
bump-pydantic --diff . # сначала смотрим diff
bump-pydantic .
- Ручные правки после bump-pydantic:
@validator→@field_validator(bump-pydantic делает это, но проверьтеpre=True→mode="before").@root_validator→@model_validator(mode="before")илиmode="after".class Config→model_config = ConfigDict(...)..dict()→.model_dump(),.json()→.model_dump_json().parse_obj()→model_validate(),parse_raw()→model_validate_json().
- Включите deprecation warnings:
import warnings
warnings.filterwarnings("error", category=DeprecationWarning, module="pydantic")
- Прогоните тесты с strict=True на критичных моделях — это выявит неявные coercion (строка → int), которые v2 по умолчанию всё ещё делает в lax mode.
Новые фичи: computed_field, JSON Schema, strict mode
computed_field — вычисляемое поле, которое попадает в сериализацию и JSON Schema, но не принимается на вход:
from pydantic import BaseModel, computed_field
class Rectangle(BaseModel):
width: float
height: float
@computed_field
@property
def area(self) -> float:
return self.width * self.height
r = Rectangle(width=3, height=4)
print(r.model_dump())
# {'width': 3.0, 'height': 4.0, 'area': 12.0}
JSON Schema — генерация стала точнее и поддерживает OpenAPI 3.1:
print(Rectangle.model_json_schema())
# Включает 'area' как readOnly property
Strict mode — запрещает неявное приведение типов:
from pydantic import BaseModel, ConfigDict
class StrictUser(BaseModel):
model_config = ConfigDict(strict=True)
age: int
StrictUser(age="25") # ValidationError! str не приводится к int
StrictUser(age=25) # OK
Strict mode можно включить глобально через ConfigDict(strict=True) или точечно через Field(strict=True) на отдельных полях.
Подводные камни: breaking changes в validators
- values → info: в v1 валидатор получал
values: dictс уже провалидированными полями. В v2@field_validatorполучает только значение поля. Для доступа к другим полям используйте@model_validator(mode="after"). - each_item=True удалён: валидация элементов списка теперь через аннотацию
Annotated[list[str], BeforeValidator(fn)]. - orm_mode → from_attributes:
Config.orm_mode = TrueсталConfigDict(from_attributes=True). - __get_validators__ удалён: кастомные типы теперь через
__get_pydantic_core_schema__. - Порядок валидации: в v1 поля валидировались в порядке объявления, в v2 порядок не гарантирован (параллелизм в Rust-ядре). Не полагайтесь на порядок в
field_validator.
# v1 — работало, потому что 'name' валидировался раньше 'greeting'
@validator("greeting", pre=False)
def set_greeting(cls, v, values):
return f"Hello, {values['name']}"
# v2 — правильный способ
@model_validator(mode="after")
def set_greeting(self) -> "MyModel":
self.greeting = f"Hello, {self.name}"
return self
Выводы
Pydantic v2 — объективно лучше: быстрее, строже, с более чистым API. Но миграция требует внимания:
- Начните с
bump-pydantic— он закроет 80% механических изменений. - Переведите
root_validator→model_validatorвручную, проверяя логику доступа к полям. - Включите
strict=Trueна критичных моделях — найдёте скрытые баги с coercion. - Используйте
computed_fieldвместо хаков с__init__и property. - Не полагайтесь на порядок валидации полей — это главный источник багов при миграции.
Если проект на FastAPI — обновляйтесь до FastAPI 0.100+, он нативно поддерживает Pydantic v2 и автоматически генерирует OpenAPI 3.1 схемы.