2025, Dec 17 13:00
Avoiding Pydantic schema generation failures from nested defaultdict annotations (Python 3.12)
Learn how to fix Pydantic 2.11 schema errors from nested defaultdict annotations in Python 3.12: use Dict annotations and a defaultdict default_factory.
Nested defaultdict annotations look perfectly fine to a static type checker, yet they can sink your Pydantic model at class creation time. With Python 3.12 and Pydantic 2.11.0, a model field annotated as a defaultdict of defaultdicts fails during schema generation, even though pyright accepts the types. The result is a class that can’t even be defined, let alone instantiated.
Reproducing the issue
The snippet below uses a nested defaultdict keyed by UUID at two levels. The typechecker is happy, but Pydantic fails when building the model’s schema.
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))),
]
# static type checking says this is fine
probe = DemoCase(payload=defaultdict[UUID, defaultdict[UUID, dict]](lambda: defaultdict(dict)))
At runtime, Pydantic tries to introspect the nested mapping and raises an error while generating the schema for the field.
pydantic.errors.PydanticSchemaGenerationError: Unable to infer a default factory for keys of type <class 'dict'>. Only str, int, bool, list, dict, frozenset, tuple, float, set are supported, other types require an explicit default factory set using `DefaultDict[..., Annotated[..., Field(default_factory=...)]]`
What’s actually going on
Static typing and runtime validation are different concerns. Pyright verifies the declared types and the factory expressions, and it passes. Pydantic, however, must generate a schema at runtime. When the annotation itself is a nested defaultdict, the library struggles to infer how to handle default values inside that structure and fails during model construction. In other words, the class definition line is where it breaks, before any instance exists.
The fix
The practical way around this is to separate the type annotation from the runtime behavior. Annotate the field as a regular Dict so Pydantic can build a schema, but keep using defaultdict as the default_factory to preserve the desired runtime semantics. This keeps type checking acceptable and allows Pydantic to complete schema generation.
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))),
]
# still type-checks and preserves runtime behavior via defaultdict
probe = DemoCase(payload=defaultdict[UUID, defaultdict[UUID, dict]](lambda: defaultdict(dict)))
Why you should care
This pattern avoids class construction failures while retaining the ergonomics of defaultdict at runtime. It also keeps your code compatible with static analysis tools like pyright without forcing you to abandon the runtime behavior you want. Pydantic handles Dict annotations well during schema generation, whereas nested defaultdict annotations trigger the failure shown above.
Takeaways
If you use nested defaultdict in Pydantic models and see schema generation errors, describe your field types with Dict in annotations and supply defaultdict via default_factory. This aligns static typing and runtime behavior in a way that Pydantic accepts, and it works in the environment observed here with Python 3.12 and Pydantic 2.11.0.