Небольшое введение к тому, как мы собственно до всего этого дошли.
Современная экосистема Python переживает большую трансформацию в подходах к обработке, валидации и (де)сериализации данных. Еще совсем недавно (десять лет назад) в питоне не было аннотаций типов, все использовали ручные проверки типов, да и в принципе мало кто заморачивался с контрактами для данных.
С появлением аннотаций типов в 3.5 версии, все потихоньку начало меняться – аннотации типов начали указывать везде, и сейчас, мало кто представляет жизнь без них. Их появление, собственно, открыло возможность декларативного программирования.
И самая очевидная тема для создания инструментов: создать удобное решение для валидации, (де)сериализации и обработки данных.
Pydantic
Несколько лет назад, начал набирать огромную популярность FastAPI (фреймворк для создания API), и в нем все завязано на pydantic. Декларативное описание схем оказалось настолько удобным, что все начали использовать его повсеместно – от конфигов до ORM-схем, и прочих сущностей в которых содержатся какие-либо данные.
Такой подход в условиях тренда сообщества на чистую архитектуру обнажил одну из главных проблем – нарушение SRP.
Это привело к созданию толстых моделей, которые:
начали сильно перегружать ответственностью и теперь модель знает: как конкретно она валидируется, как сериализуется в JSON, как генерирует json-схему (содержит описание самой себя для внешнего мира)
стали антипаттерном для производительности: каждое создание экземпляра модели проходит весь цикл валидации и требует от 240% до 430% больше времени по сравнению с обычным dataclasses, и за счет хранения дополнительных данных (из пункта 1) требуют большего количества оперативной памяти
могут приводить к проблемам связанным с максимальной лояльностью pydantic к входным данным за счет неявного преобразования, например, если вы передаете
floatв поле с типомDecimal, что может привести к неточностям для денежных вычислений.
Adaptix
Эта библиотека появилась сравнительно давно (первый коммит был в сентябре 2018 года), и ранее она называлась dataclass-factory.
Adaptix предлагает принципиально иной подход к работе с моделями, разделяя сами модели (модель остается обычным чистым python объектом) и работу с загрузкой данных и их (де)сериализации. Вся логика преобразования, валидации и маппинга инкапсулируется в отдельном объекте – Retort (реторта), которую обычно создают один раз при бутстрапе приложения.
Такой подход решает проблему SRP: остается чистая модель данных и реторта, в которой содержатся рецепты – список провайдеров, которые указывают, как работать с теми или иными случаями.
В библиотеке есть мощная система предикатов, которая позволяет делать рецепты не только к конкретным моделям / типам данных, но и к определенным паттернам. Например, вам в приложении всегда необходимо сериализовывать created_at в timestamp число, рецепт будет выглядеть следующим образом:
from adaptix import Retort, dumper, P
retort = Retort(
recipe=[
dumper(P.created_at, lambda x: int(x.timestamp()))
]
)
Вопрос с производительностью у Adaptix решается иначе: при первом обращении к реторте с определенным типом данных на выгрузку / загрузку, происходит генерация оптимизированной функции, которая в дальнейшем выполняет обработку данных с этим типом данных.
Сравнение Pydantic и Adaptix
Архитектура
Pydantic
Удобно, легко и очень быстро описывать модель – сразу видны все правила работы с данными и хорошо применяется при необходимости работы с данными из внешних систем.
Однако, в больших и долгоживущих приложениях тяжело поддерживать модели за счет загрязнения и необходимости учитывать все существующие детали работы с моделью (алиасы, валидаторы, (де)сериализаторы для внешних и внутренних систем).
Adaptix
Ваша модель данных остается чистым датаклассом, а правила работы с данными выносятся в реторты, которые можно разделять на слои приложения – для инфраструктуры использовать чистую загрузку модели без валидации, для презентационного слоя использовать валидаторы, подстраивать правила (де)сериализации данных в зависимости от требований.
Минус, на мой взгляд, довольно незначительный – требует большего количества кода для старта работы с моделью.
Производительность
Pydantic
В случае с парсингом тяжелых JSON, выигрывает, за счет осуществления парсинга внутри ядра на Rust.
Независимо от источника данных (бд / внешняя система) требует при каждом создании инстанса модели проходить весь цикл валидации, что сильно замедляет создание инстанса и дальнейшую работу с ним.
Adaptix
Работает с объектами со скоростью нативного питона, создает обычные инстансы моделей (классы, датаклассы, в зависимости от того, что вы используете).
В случае с парсингом JSON, то минусом является необходимость самостоятельно осуществлять парсинг за счет json / ujson / orjson / прочих парсеров.
Возьмем здесь простой бенчмарк из документации adaptix:
from dataclasses import dataclass
from datetime import datetime
from timeit import timeit
from typing import Optional
from pydantic import BaseModel, PositiveInt
class UserPydantic(BaseModel):
id: int
name: str = "John Doe"
signup_ts: Optional[datetime]
tastes: dict[str, PositiveInt]
@dataclass(kw_only=True)
class UserDataclass:
id: int
name: str = "John Doe"
signup_ts: Optional[datetime]
tastes: dict[str, PositiveInt]
stmt = """
User(
id=123,
signup_ts=datetime(year=2019, month=6, day=1, hour=12, minute=22),
tastes={'wine': 9, 'cheese': 7, 'cabbage': '1'},
)
"""
print("dataclasses:", timeit(stmt, globals={"User": UserDataclass, "datetime": datetime}))
print("Pydantic (standard):", timeit(stmt, globals={"User": UserPydantic, "datetime": datetime}))
print("Pydantic (model_construct):", timeit(stmt, globals={"User": UserPydantic.model_construct, "datetime": datetime}))

Мы получаем следующие результаты:
pydantic (standard) – медленнее обычного dataclasses в 2.15 раз
pydantic (model_construct) – медленнее обычного dataclasses в 3.14 раз
model_construct участвует в этом бенчмарке, так как это действие должно ускорять создание инстанса модели в обход валидации.
Гибкость валидации
Pydantic
Валидация описывается внутри самого класса через декоратор @validator и чтобы изменить правило для того же поля, но в другом контексте – необходимо делать класс-наследник и менять уже там.
Adaptix
Правила валидации описываются снаружи в реторте, не затрагивая сам класс и позволяя использовать систему предикатов для создания общих правил по работе с определенными типами данных / полями моделей.
Экосистема и интеграция
Pydantic
FastAPI – поддерживает нативно, IDE (PyCharm / VSCode) – раньше требовались плагины для работы с pydantic моделями, сейчас движутся в сторону нативной работы с pydantic, но моментами бывает, что это работает весьма криво.
Adaptix
Если использовать его в FastAPI, то требует некоторой настройки с участием Dependency Injection, которая заключается в необходимости преобразовывания получаемого JSON в нужную модель данных через реторту. Это можно сделать написав общий лоадер, куда можно передавать тип модели, либо делать отдельные функции и указывать их в качестве зависимостей в контроллерах.
Для обработок ошибок валидации потребуется написать свой exception_handler, который будет перехватывать ошибки adaptix и выдавать их в структурированном ответе.
IDE – полная поддержка за счет того, что библиотека полностью типизирована и работает с обычными датаклассами.
Выводы
Необязательно выбирать что-то одно. Можно использовать Pydantic для работы в API контроллерах, где нужна генерация OpenAPI схемы и быстрый парсинг JSON. Adaptix стоит использовать с чистыми датаклассами внутри самого приложения, чтобы гонять данные между слоев без задержек на все проверки.
Дополнительные материалы:
https://adaptix.readthedocs.io/en/latest/why-not-pydantic.html – почему не Pydantic, от автора самой библиотеки adaptix.
