sphinx-doc / sphinx-doc/sphinx
Sphinx autodoc contaminates pydantic BaseModel annotations, breaking `extra="allow"`
Nobody has claimed this yet.
- 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
- clone the following gist https://gist.github.com/dcermak/4edc5f80b9f5e32d8f0de7524a80e542
uv run sphinx-build -a -W -T -b html doc/source/ build/- you will get a giant ugly stack trace in the middle of the output:
# 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:
- Uses
ModuleAnalyzer.for_module('pydantic.main')to parse the source file - Extracts raw string annotations, including
__pydantic_extra__: Dict[str, Any] | None - At line 94, does
cls.__annotations__ = annotationsonBaseModel, 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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