Производительность Pydantic v2: миграция с v1 и реальная оптимизация
Pydantic v2 обещал ускорение валидации в 5–50 раз за счёт Rust-ядра. На практике после миграции реального проекта с v1 на v2 часть моделей ускорилась в 17 раз, а часть — замедлилась. Разбираю, где прирост реален, что ломается при переезде и какие приёмы дают максимальную отдачу.

Почему v1 стала узким местом
В проекте на FastAPI 0.100+ с сотней Pydantic-моделей профилирование показало, что до 40% времени обработки запроса уходило на валидацию и сериализацию. Модели с вложенными списками, Optional-полями и кастомными валидаторами генерировали тысячи промежуточных объектов. Pydantic v1 делал всё на чистом Python: каждый вызов __init__ проходил через цепочку validator-декораторов, а сериализация через .dict() рекурсивно обходила дерево.
Pydantic v2 (начиная с 2.0, актуальная на момент написания — 2.7.x) перенёс ядро валидации в библиотеку pydantic-core, написанную на Rust. Это не просто «переписали на другом языке» — изменилась модель данных, API валидаторов и даже поведение при сериализации.
Что реально изменилось в архитектуре
Ключевое отличие — появление CoreSchema. В v1 Pydantic строил валидационную цепочку из Python-объектов при импорте модуля. В v2 при первом обращении к модели формируется Rust-схема, которая затем переиспользуется без возврата в Python. Отсюда две вещи:
- Холодный старт модели стал дороже — построение CoreSchema занимает больше времени, чем создание Python-валидаторов в v1.
- Каждый последующий вызов валидации — значительно быстрее, потому что горячий путь целиком в Rust.
Для HTTP-сервиса, который создаёт экземпляры моделей тысячи раз в секунду, это чистый выигрыш. Для скрипта, который импортирует 300 моделей и создаёт по одному экземпляру каждой, — может быть проигрыш на старте.
Сериализация: .dict() больше нет
Метод .dict() заменён на .model_dump(), а .json() — на .model_dump_json(). Разница не только в имени: .model_dump_json() сериализует напрямую в байтовую строку через Rust, минуя промежуточный Python-словарь. Это критично для эндпоинтов, которые отдают большие списки объектов.
Замеры: до и после
Замеры сделаны на MacBook Pro M2, Python 3.12, Pydantic v1.10.15 → v2.7.4. Модель — реальный объект заказа с 18 полями, тремя вложенными моделями и списком из 10 элементов. Каждый тест — 100 000 итераций, медиана пяти прогонов.
| Операция | v1, мкс/вызов | v2, мкс/вызов | Ускорение |
|---|---|---|---|
| Создание из dict | 48 | 2.8 | ×17 |
| Создание из JSON-строки | 62 | 3.1 | ×20 |
| Сериализация в dict | 34 | 4.2 | ×8 |
| Сериализация в JSON | 51 | 1.9 | ×27 |
| Модель с 5 кастомными валидаторами | 71 | 12 | ×6 |
Модель с __get_validators__ (v1-совместимость) | 71 | 68 | ×1.04 |
Последняя строка — ловушка. Если вы мигрировали модель, но оставили старый валидатор через слой совместимости, Rust-ядро не используется: вызов проваливается в Python, и выигрыш исчезает.
Самая частая причина, по которой v2 «не быстрее» — незавершённая миграция. Одна модель с__get_validators__или@validatorвместо@field_validatorтянет весь запрос обратно в Python.
Миграция: что ломается и как чинить
Шаг 1 — переименования API
Основные замены, которые покрывают 80% случаев:
@validator→@field_validator(аргументvaluesзаменён наinfo: FieldValidationInfo)@root_validator→@model_validator.dict()→.model_dump().json()→.model_dump_json().parse_obj()→model_validate().parse_raw()→model_validate_json()class Config:→model_config = ConfigDict(...)
Пакет bump-pydantic автоматизирует часть замен через AST-трансформации. Он не покроет кастомные типы, но избавит от рутины.
Шаг 2 — кастомные типы
В v1 кастомный тип реализовывал __get_validators__. В v2 нужен метод __get_pydantic_core_schema__, который возвращает CoreSchema — Rust-совместимое описание валидации. Это самая болезненная часть миграции.
from pydantic import GetCoreSchemaHandler
from pydantic_core import core_schema
class TrimmedStr(str):
"""Строка, которая автоматически обрезает пробелы по краям."""
@classmethod
def __get_pydantic_core_schema__(
cls,
source_type: type,
handler: GetCoreSchemaHandler,
) -> core_schema.CoreSchema:
return core_schema.no_info_plain_validator_function(
cls._validate,
serialization=core_schema.to_string_ser_schema(),
)
@classmethod
def _validate(cls, value: object) -> "TrimmedStr":
if not isinstance(value, str):
raise ValueError("expected str")
return cls(value.strip())
Обратите внимание: core_schema импортируется из pydantic_core, а не из pydantic. Точные имена функций-конструкторов схем меняются между минорными версиями — сверяйтесь с документацией pydantic-core для вашей версии.
Шаг 3 — orm_mode и from_orm
Параметр orm_mode = True заменён на from_attributes = True в ConfigDict. Метод .from_orm() удалён — используйте model_validate(obj, from_attributes=True).
Оптимизации, которые дают больше всего
model_validate_json вместо json.loads + model_validate
Распространённый паттерн в v1-коде:
import json
data = json.loads(raw_bytes)
order = Order(**data)
В v2 замените на:
order = Order.model_validate_json(raw_bytes)
Разница — один проход через Rust вместо двух проходов через Python. На модели заказа это 3.1 мкс вместо 7.4 мкс: экономия больше 50% на каждом входящем запросе.
TypeAdapter для коллекций
Если вы валидируете список объектов, не оборачивайте его в модель-обёртку. TypeAdapter создаёт Rust-схему для произвольного типа:
from pydantic import TypeAdapter
# Создаётся один раз на уровне модуля
orders_adapter = TypeAdapter(list[Order])
# Валидация списка из JSON — один вызов в Rust
orders = orders_adapter.validate_json(raw_bytes)
Ключевой момент: TypeAdapter нужно создавать один раз и переиспользовать. Создание адаптера в теле функции на каждый запрос убивает весь выигрыш — построение CoreSchema каждый раз обходится дорого.
Strict-режим
По умолчанию v2 работает в lax-режиме: строка "42" конвертируется в int. Если вы контролируете источники данных и уверены в типах, включите strict-режим — он пропускает coercion и работает быстрее:
from pydantic import ConfigDict
class Order(BaseModel):
model_config = ConfigDict(strict=True)
# ...
На модели с числовыми полями strict-режим дал ещё 15–20% сверху. Но будьте готовы к тому, что данные из JSON (где все числа — float или int, а не строки) обычно и так проходят strict-валидацию, а вот данные из форм и query-параметров — нет.
Где подход ломается
- Динамические модели. Если вы строите модели через
create_model()на лету (например, для каждого запроса с разной схемой), холодный старт CoreSchema съест весь выигрыш. В проекте, где модели создавались динамически, v2 оказалась на 30% медленнее v1 до тех пор, пока не добавили LRU-кеш наcreate_model. - Глубокие рекурсивные модели. Модель, ссылающаяся на себя через
list["TreeNode"], в v2 требует отложенной сборки схемы (model_rebuild()). Если забыть вызватьmodel_rebuild()после определения всех моделей с forward references, получитеPydanticUndefinedAnnotation. - Совместимость с ORM. SQLAlchemy-модели с lazy-loaded relationships ломают
from_attributes=True, если сессия уже закрыта. В v1 это тоже было проблемой, но в v2 ошибка приходит из Rust-слоя и менее информативна.
* CoreSchema — внутреннее JSON-подобное описание валидационных правил, которое Pydantic v2 передаёт в Rust-ядро. Вы не работаете с ним напрямую, кроме случаев кастомных типов.
Что делать дальше
После миграции стоит пройтись по трём точкам:
- Найти все оставшиеся
@validator— каждый из них означает, что модель не использует Rust-путь. Командаgrep -rn "@validator" --include="*.py"покажет масштаб. - Добавить бенчмарк валидации в CI. Простой
pytest-benchmarkтест на создание 1 000 экземпляров основной модели покажет регрессию при обновлении зависимостей. - Измерить эффект
model_validate_jsonна реальном трафике: если сервис принимает JSON, переход сjson.loads+ конструктор на прямой вызов — самый дешёвый и заметный рычаг.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: Pydantic V2 Migration Guide — официальная документация, pydantic-core — репозиторий Rust-ядра Pydantic
Частые вопросы
Можно ли использовать Pydantic v1 и v2 в одном проекте одновременно?
bump-pydantic автоматически мигрирует весь код?
Что будет, если я обновлю Pydantic, но не поменяю код?
У меня другая версия Python — v2 будет работать?
Не замедлит ли миграция на v2 запуск приложения?
Есть ли подводные камни при использовании v2 в продакшене?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.