pythonpydantic

Unable to create a nested DefaultDict in a pydantic BaseModel


Consider:

#!/usr/bin/env -S uv run --script
# /// script
# dependencies = [ "pydantic>=2.10.5,<3" ]
# requires-python = ">=3.12,<3.13"
# ///

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


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


# to validate that the typechecker thinks the above are valid:
instance = Repro(value=defaultdict[UUID, defaultdict[UUID, dict]](lambda: defaultdict(dict)))

The above passes type checking with pyright, but at runtime, the class definition fails while pydantic is trying to introspect the object's schema:

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=...)]]


Thinking it might be introspection of the inner defaultdict triggering the error message, I tried adding a pydantic.Field to its type via an inner annotation:

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

...but with identical effect.


The above is observed with Python 3.12 and Pydantic 2.11.0, type-checked with pyright 1.1.382.

The full stack trace follows:

Traceback (most recent call last):
  File "/Users/chaduffy/repro.py", line 13, in <module>
    class Repro(pydantic.BaseModel):
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_model_construction.py", line 237, in __new__
    complete_model_class(
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_model_construction.py", line 597, in complete_model_class
    schema = gen_schema.generate_schema(cls)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py", line 706, in generate_schema
    schema = self._generate_schema_inner(obj)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py", line 999, in _generate_schema_inner
    return self._model_schema(obj)
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py", line 832, in _model_schema
    {k: self._generate_md_field_schema(k, v, decorators) for k, v in fields.items()},
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py", line 1201, in _generate_md_field_schema
    common_field = self._common_field_schema(name, field_info, decorators)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py", line 1367, in _common_field_schema
    schema = self._apply_annotations(
             ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py", line 2279, in _apply_annotations
    schema = get_inner_schema(source_type)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_schema_generation_shared.py", line 83, in __call__
    schema = self._handler(source_type)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py", line 2261, in inner_handler
    schema = self._generate_schema_inner(obj)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py", line 1004, in _generate_schema_inner
    return self.match_type(obj)
           ^^^^^^^^^^^^^^^^^^^^
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py", line 1118, in match_type
    return self._match_generic_type(obj, origin)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py", line 1157, in _match_generic_type
    return self._mapping_schema(origin, *self._get_first_two_args_or_any(obj))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py", line 571, in _mapping_schema
    values_schema = self.generate_schema(values_type)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py", line 706, in generate_schema
    schema = self._generate_schema_inner(obj)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py", line 1004, in _generate_schema_inner
    return self.match_type(obj)
           ^^^^^^^^^^^^^^^^^^^^
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py", line 1118, in match_type
    return self._match_generic_type(obj, origin)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py", line 1157, in _match_generic_type
    return self._mapping_schema(origin, *self._get_first_two_args_or_any(obj))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py", line 583, in _mapping_schema
    default_default_factory = get_defaultdict_default_default_factory(values_type)
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_validators.py", line 482, in get_defaultdict_default_default_factory
    default_default_factory = infer_default()
                              ^^^^^^^^^^^^^^^
  File "/Users/chaduffy/.cache/uv/archive-v0/L2nssVviE9tesQFv5NzrQ/lib/python3.12/site-packages/pydantic/_internal/_validators.py", line 466, in infer_default
    raise PydanticSchemaGenerationError(
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=...)]]`

For further information visit https://errors.pydantic.dev/2.11/u/schema-for-unknown-type

Solution

  • Use Dict from typing in the annotation. By using Dict in the type annotation but defaultdict in the default factory, we're separating the static type checking from the runtime behavior. Pydantic struggles with generating schemas for nested defaultdict, but it handles regular Dict types well.

    import pydantic
    from typing import Annotated, Dict
    from collections import defaultdict
    from uuid import UUID
    
    
    class Repro(pydantic.BaseModel):
        value: Annotated[
            Dict[UUID, Dict[UUID, dict]],
            pydantic.Field(default_factory=lambda: defaultdict[UUID, defaultdict[UUID, dict]](lambda: defaultdict(dict))),
        ]
    
    
    # to validate that the typechecker thinks the above are valid:
    instance = Repro(value=defaultdict[UUID, defaultdict[UUID, dict]](lambda: defaultdict(dict)))