The docstring is lost when setting type parameters on a generic.
Minimal example:
from typing import TypeVar, Generic
T = TypeVar("T")
class Test(Generic[T]):
"""My docstring"""
assert Test.__doc__ == "My docstring"
This works fine as expected. However, this fails as __doc__
is now None
:
assert Test[int].__doc__ == "My docstring"
I would expect the docstring to still be the same. Afterall, it's still the "same" class.
Is there something I just don't understand about how Python's typing system making this intendet behavior?
Using parameterized types in FastAPI, I'm losing the description (coming from the docstring) when generating OpenAPI specs.
I can fix this with a decorator, but that leads to more problems in my case with Pydantic's model creation. But that's besides the point. I'd like to understand why this is happening in the first place regardless of non-builtin tools.
Parameterising user-defined generic types with type arguments results in instances of typing._GenericAlias
, which inherits from typing._BaseGenericAlias
.
>>> type(Test[int])
<class 'typing._GenericAlias'>
>>> type(Test[int]).mro()
[<class 'typing._GenericAlias'>, <class 'typing._BaseGenericAlias'>, <class 'typing._Final'>, <class 'object'>]
If you look at the source code, it shows you that attributes are relayed from Test
to Test[int]
via a __getattr__
defined on _BaseGenericAlias
.
This reveals 2 issues preventing __doc__
from being relayed from Test
to Test[int]
:
Test[int]
is an instance of typing._GenericAlias
, when you're accessing Test[int].__doc__
, you are actually accessing typing._GenericAlias.__doc__
; since all classes (including typing._GenericAlias
) have a .__doc__ = None
if no docstring is provided, __getattr__
is never called and you get None
. If you add a dummy docstring to your Python standard library's typing.py
,
# typing.py
...
class _GenericAlias(_BaseGenericAlias, _root=True):
"""
HELLO
"""
...
you can verify the behaviour:
>>> Test[int].__doc__
'HELLO'
__getattr__
were called, the implementation deliberately avoids relaying dunder attributes (like __doc__
):
def __getattr__(self, attr):
...
# We are careful for copy and pickle.
# Also for simplicity we don't relay any dunder names
if '__origin__' in self.__dict__ and not _is_dunder(attr):
return getattr(self.__origin__, attr)
raise AttributeError(attr)