sphinx-doc / sphinx-doc/sphinx

Sphinx autodoc contaminates pydantic BaseModel annotations, breaking `extra="allow"`

Open
#14,337 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

extensions:autodoc type:regression
Dominant language
Python
Stars
8k
Forks
2.6k
PR merge metrics
No merged PRs in 30d

Description

Describe the bug

sphinx.ext.autodoc's type comment processing re-parses third-party library source files and writes raw string annotations back onto classes whose metaclasses intentionally cleared them. This breaks pydantic models using extra="allow" with a NameError: name 'Dict' is not defined

This is a regression of the new autodoc implementation and can be worked around by setting autodoc_use_legacy_class_based = True as described in #14089

How to Reproduce
# snip
File "/tmp/tmp.TVBsJfZRsg/doc/source/../../src/myapp/config.py", line 6, in <module>
    class Settings(BaseSettings):
    ...<4 lines>...
        model_config = SettingsConfigDict(extra="allow")
  File "/tmp/tmp.TVBsJfZRsg/.venv/lib64/python3.14/site-packages/pydantic/_internal/_model_construction.py", line 255, in __new__
    complete_model_class(
    ~~~~~~~~~~~~~~~~~~~~^
        cls,
        ^^^^
    ...<3 lines>...
        create_model_module=_create_model_module,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/tmp/tmp.TVBsJfZRsg/.venv/lib64/python3.14/site-packages/pydantic/_internal/_model_construction.py", line 648, in complete_model_class
    schema = gen_schema.generate_schema(cls)
  File "/tmp/tmp.TVBsJfZRsg/.venv/lib64/python3.14/site-packages/pydantic/_internal/_generate_schema.py", line 729, in generate_schema
    schema = self._generate_schema_inner(obj)
  File "/tmp/tmp.TVBsJfZRsg/.venv/lib64/python3.14/site-packages/pydantic/_internal/_generate_schema.py", line 1023, in _generate_schema_inner
    return self._model_schema(obj)
           ~~~~~~~~~~~~~~~~~~^^^^^
  File "/tmp/tmp.TVBsJfZRsg/.venv/lib64/python3.14/site-packages/pydantic/_internal/_generate_schema.py", line 816, in _model_schema
    extras_annotation = _typing_extra.eval_type_backport(
        _typing_extra._make_forward_ref(
    ...<2 lines>...
        *self._types_namespace,
    )
  File "/tmp/tmp.TVBsJfZRsg/.venv/lib64/python3.14/site-packages/pydantic/_internal/_typing_extra.py", line 455, in eval_type_backport
    return _eval_type_backport(value, globalns, localns, type_params)
  File "/tmp/tmp.TVBsJfZRsg/.venv/lib64/python3.14/site-packages/pydantic/_internal/_typing_extra.py", line 492, in _eval_type_backport
    return _eval_type(value, globalns, localns, type_params)
  File "/tmp/tmp.TVBsJfZRsg/.venv/lib64/python3.14/site-packages/pydantic/_internal/_typing_extra.py", line 524, in _eval_type
    evaluated = typing._eval_type(  # type: ignore
        value,
    ...<9 lines>...
        prefer_fwd_module=True,
    )
  File "/usr/lib64/python3.14/typing.py", line 461, in _eval_type
    return evaluate_forward_ref(t, globals=globalns, locals=localns,
                                type_params=type_params, owner=owner,
                                _recursive_guard=recursive_guard, format=format)
  File "/usr/lib64/python3.14/typing.py", line 1000, in evaluate_forward_ref
    value = forward_ref.evaluate(globals=globals, locals=locals,
                                 type_params=type_params, owner=owner, format=format)
  File "/usr/lib64/python3.14/annotationlib.py", line 205, in evaluate
    return eval(code, globals=globals, locals=locals)
  File "<string>", line 1, in <module>
NameError: name 'Dict' is not defined. Did you mean: 'dict'?

The above exception was the direct cause of the following exception:
# snip
Environment Information
- Sphinx 9.1.0
- pydantic 2.12.5
- pydantic-settings 2.13.1
- Python 3.13+
Sphinx extensions
sphinx.ext.autodoc
Additional context

Full disclosure, the following was found with the help of Claude Opus and verified by me

The bug is in , in the function _ensure_annotations_from_type_comments in sphinx/ext/autodoc/_dynamic/_type_comments.py

When autodoc processes myapp.models, it eventually calls _load_object_by_name for each member (e.g., MyModel). This calls _make_props_from_imported_object which calls _ensure_annotations_from_type_comments(parent) (defined in: sphinx/ext/autodoc/_dynamic/_type_comments.py)

That function walks the entire MRO of MyModel, including pydantic.BaseModel, see: https://github.com/sphinx-doc/sphinx/blob/cc7c6f435ad37bb12264f8118c8461b230e6830c/sphinx/ext/autodoc/_dynamic/_type_comments.py#L30

For BaseModel, it finds module pydantic.main and calls _update_module_annotations_from_type_comments on it. That function:

  1. Uses ModuleAnalyzer.for_module('pydantic.main') to parse the source file
  2. Extracts raw string annotations, including __pydantic_extra__: Dict[str, Any] | None
  3. At line 94, does cls.__annotations__ = annotations on BaseModel, writing the parsed string annotations back

This is destructive because pydantic's ModelMetaclass.__new__ intentionally clears __annotations__. Sphinx undoes this by re-parsing the source and writing the raw strings back.

Later, when myapp.config is imported and Settings(BaseSettings, extra="allow") is constructed, pydantic's schema generator walks the MRO looking for __pydantic_extra__ in __annotations__. It now finds the string 'Dict[str, Any] | None' that Sphinx wrote back onto BaseModel. Pydantic tries to evaluate this as a forward reference in myapp.config's namespace, where Dict is not imported, causing the NameError.

Possible fixes

  • Skip classes whose module originates from site-packages or outside the documented project
  • Do not overwrite cls.__annotations__ when the existing annotations dict was intentionally set (e.g., check if __annotations__ is already in the class __dict__ and is empty, which may indicate intentional clearing by a metaclass)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with sphinx/ext/autodoc/_dynamic/type_comments.py, especially _ensure_annotations_from_type_comments and _update_module_annotations_from_type_comments, then follow their callers in sphinx/ext/autodoc/_dynamic/_loader.py. Reproduce the issue with the provided gist and sphinx-build command; done means autodoc no longer causes the pydantic settings import to fail with the reported NameError while processing the model.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
documentation
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.