2026, Jan 09 12:03

Вложенный defaultdict в Pydantic 2.11 на Python 3.12: как исправить ошибку генерации схемы

Почему вложенный defaultdict ломает генерацию схемы в Pydantic 2.11 и как это исправить: аннотируйте Dict, а default_factory оставьте через defaultdict.

Вложенные аннотации defaultdict выглядят безупречно для статического анализатора, но на этапе создания класса они могут «утопить» вашу модель Pydantic. В связке Python 3.12 и Pydantic 2.11.0 поле модели, аннотированное как defaultdict из defaultdict, падает при генерации схемы, хотя pyright типы принимает. В итоге класс невозможно даже определить, не говоря уже о создании экземпляра.

Воспроизводим проблему

Ниже фрагмент кода с вложенным defaultdict, где на обоих уровнях ключи — UUID. Типизатор доволен, а Pydantic падает при сборке схемы модели.

import pydantic
from typing import Annotated
from collections import defaultdict
from uuid import UUID


class DemoCase(pydantic.BaseModel):
    payload: Annotated[
        defaultdict[UUID, defaultdict[UUID, dict]],
        pydantic.Field(default_factory=lambda: defaultdict[UUID, defaultdict[UUID, dict]](lambda: defaultdict(dict))),
    ]


# статическая проверка типов считает это корректным
probe = DemoCase(payload=defaultdict[UUID, defaultdict[UUID, dict]](lambda: defaultdict(dict)))

Во время выполнения Pydantic пытается проинспектировать вложенное отображение и выбрасывает ошибку при генерации схемы для поля.

pydantic.errors.PydanticSchemaGenerationError: Не удаётся вывести фабрику значений по умолчанию для ключей типа <class 'dict'>. Поддерживаются только str, int, bool, list, dict, frozenset, tuple, float, set; для других типов требуется явная фабрика по умолчанию, заданная через `DefaultDict[..., Annotated[..., Field(default_factory=...)]]`

Что происходит на самом деле

Статическая типизация и проверка во время выполнения — разные задачи. Pyright сверяет объявленные типы и фабрики и остаётся доволен. Pydantic же обязан на лету сгенерировать схему. Когда в аннотации фигурирует вложенный defaultdict, библиотека не может вывести, как обрабатывать значения по умолчанию внутри такой структуры, и падает прямо при построении модели. Иными словами, всё ломается уже на строке с определением класса, до появления каких‑либо экземпляров.

Решение

Практичный обходной путь — развести аннотацию типов и поведение во время выполнения. Аннотируйте поле как обычный Dict, чтобы Pydantic смог собрать схему, а для default_factory продолжайте использовать defaultdict, сохраняя нужную семантику на рантайме. Так вы удовлетворите статическую проверку и дадите Pydantic завершить генерацию схемы.

import pydantic
from typing import Annotated, Dict
from collections import defaultdict
from uuid import UUID


class DemoCase(pydantic.BaseModel):
    payload: Annotated[
        Dict[UUID, Dict[UUID, dict]],
        pydantic.Field(default_factory=lambda: defaultdict[UUID, defaultdict[UUID, dict]](lambda: defaultdict(dict))),
    ]


# по-прежнему проходит проверку типов и сохраняет поведение во время выполнения за счёт defaultdict
probe = DemoCase(payload=defaultdict[UUID, defaultdict[UUID, dict]](lambda: defaultdict(dict)))

Почему это важно

Этот приём позволяет избежать сбоев при построении класса и при этом сохранить удобство defaultdict на рантайме. Он также держит код совместимым со статическими анализаторами вроде pyright, не вынуждая отказываться от желаемого поведения во время выполнения. Pydantic уверенно работает с аннотациями Dict при генерации схемы, тогда как вложенные аннотации defaultdict приводят к описанной выше ошибке.

Выводы

Если вы используете вложенные defaultdict в моделях Pydantic и сталкиваетесь с ошибками генерации схемы, описывайте типы полей через Dict в аннотациях, а defaultdict передавайте через default_factory. Так вы согласуете статическую типизацию и поведение на рантайме в форме, приемлемой для Pydantic; решение работает в окружении с Python 3.12 и Pydantic 2.11.0.