How to access the type arguments of typing.Generic?
Python >= 3.8
As of Python3.8 there is typing.get_args
:
print( get_args( List[int] ) ) # (<class 'int'>,)
PEP-560 also provides __orig_bases__[n]
, which allows us the arguments of the nth generic base:
from typing import TypeVar, Generic, get_args
T = TypeVar( "T" )
class Base( Generic[T] ):
pass
class Derived( Base[int] ):
pass
print( get_args( Derived.__orig_bases__[0] ) ) # (<class 'int'>,)
Python >= 3.6
As of Python 3.6. there is a public __args__
and (__parameters__
) field.
For instance:
print( typing.List[int].__args__ )
This contains the generic parameters (i.e. int
), whilst __parameters__
contains the generic itself (i.e. ~T
).
Python < 3.6
Use typing_inspect.getargs
Some considerations
typing
follows PEP8. Both PEP8 and typing
are coauthored by Guido van Rossum. A double leading and trailing underscore is defined in as: "“magic” objects or attributes that live in user-controlled namespaces".
The dunders are also commented in-line; from the official repository for typing we
can see:
- "
__args__
is a tuple of all arguments used in subscripting, e.g.,Dict[T, int].__args__ == (T, int)
".
However, the authors also note:
- "The typing module has provisional status, so it is not covered by the high standards of backward compatibility (although we try to keep it as much as possible), this is especially true for (yet undocumented) dunder attributes like
__union_params__
. If you want to work with typing types in runtime context, then you may be interested in thetyping_inspect
project (part of which may end up in typing later)."
I general, whatever you do with typing
will need to be kept up-to-date for the time being. If you need forward compatible changes, I'd recommend writing your own annotation classes.
Generic[T] base class - how to get type of T from within instance?
There is no supported API for this. Under limited circumstances, if you're willing to mess around with undocumented implementation details, you can sometimes do it, but it's not reliable at all.
First, mypy doesn't require you to provide type arguments when assigning to a generically-typed variable. You can do things like x: Test[int] = Test()
and neither Python nor mypy will complain. mypy infers the type arguments, but Test
is used at runtime instead of Test[int]
. Since explicit type arguments are awkward to write and carry a performance penalty, lots of code only uses type arguments in the annotations, not at runtime.
There's no way to recover type arguments at runtime that were never provided at runtime.
When type arguments are provided at runtime, the implementation does currently try to preserve this information, but only in a completely undocumented internal attribute that is subject to change without notice, and even this attribute might not be present. Specifically, when you call
Test[int]()
, the class of the new object is Test
rather than Test[int]
, but the typing
implementation attempts to set
obj.__orig_class__ = Test[int]
on the new object. If it cannot set __orig_class__
(for example, if Test
uses __slots__
), then it catches the AttributeError and gives up.
__orig_class__
was introduced in Python 3.5.3; it is not present on 3.5.2 and lower. Nothing in typing
makes any actual use of __orig_class__
.
The timing of the __orig_class__
assignment varies by Python version, but currently, it's set after normal object construction has already finished. You will not be able to inspect __orig_class__
during __init__
or __new__
.
These implementation details are current as of CPython 3.8.2.
__orig_class__
is an implementation detail, but at least on Python 3.8, you don't have to access any additional implementation details to get the type arguments. Python 3.8 introduced typing.get_args
, which returns a tuple of the type arguments of a typing
type, or ()
for an invalid argument. (Yes, there was really no public API for that all the way from Python 3.5 until 3.8.)
For example,
typing.get_args(Test[int]().__orig_class__) == (int,)
If __orig_class__
is present and you're willing to access it, then __orig_class__
and get_args
together provide what you're looking for.
Access type argument in any specific subclass of user-defined Generic[T] class
TL;DR
Grab the GenericBase
from the subclass' __orig_bases__
tuple, pass it to typing.get_args
, grab the first element from the tuple it returns, and make sure what you have is a concrete type.
1) Starting with get_args
As pointed out in this post, the typing
module for Python 3.8+
provides the get_args
function. It is convenient because given a specialization of a generic type, get_args
returns its type arguments (as a tuple).
Demonstration:
from typing import Generic, TypeVar, get_args
T = TypeVar("T")
class GenericBase(Generic[T]):
pass
print(get_args(GenericBase[int]))
Output:
(<class 'int'>,)
This means that once we have access to a specialized GenericBase
type, we can easily extract its type argument.
2) Continuing with __orig_bases__
As further pointed out in the aforementioned post, there is this handy little class attribute __orig_bases__
that is set by the type
metaclass when a new class is created. It is mentioned here in PEP 560
, but is otherwise hardly documented.
This attribute contains (as the name suggests) the original bases as they were passed to the metaclass constructor in the form of a tuple. This distinguishes it from __bases__
, which contains the already resolved bases as returned by types.resolve_bases
.
Demonstration:
from typing import Generic, TypeVar
T = TypeVar("T")
class GenericBase(Generic[T]):
pass
class Specific(GenericBase[int]):
pass
print(Specific.__bases__)
print(Specific.__orig_bases__)
Output:
(<class '__main__.GenericBase'>,)
(__main__.GenericBase[int],)
We are interested in the original base because that is the specialization of our generic class, meaning it is the one that "knows" about the type argument (int
in this example), whereas the resolved base class is just an instance of type
.
3) Simplistic solution
If we put these two together, we can quickly construct a simplistic solution like this:
from typing import Generic, TypeVar, get_args
T = TypeVar("T")
class GenericBase(Generic[T]):
@classmethod
def get_type_arg_simple(cls):
return get_args(cls.__orig_bases__[0])[0]
class Specific(GenericBase[int]):
pass
print(Specific.get_type_arg_simple())
Output:
<class 'int'>
But this will break as soon as we introduce another base class on top of our GenericBase
.
from typing import Generic, TypeVar, get_args
T = TypeVar("T")
class GenericBase(Generic[T]):
@classmethod
def get_type_arg_simple(cls):
return get_args(cls.__orig_bases__[0])[0]
class Mixin:
pass
class Specific(Mixin, GenericBase[int]):
pass
print(Specific.get_type_arg_simple())
Output:
Traceback (most recent call last):
...
return get_args(cls.__orig_bases__[0])[0]
IndexError: tuple index out of range
This happens because cls.__orig_bases__[0]
now happens to be Mixin
, which is not a parameterized type, so get_args
returns an empty tuple ()
.
So what we need is a way to unambiguously identify the GenericBase
from the __orig_bases__
tuple.
4) Identifying with get_origin
Just like typing.get_args
gives us the type arguments for a generic type, typing.get_origin
gives us the unspecified version of a generic type.
Demonstration:
from typing import Generic, TypeVar, get_origin
T = TypeVar("T")
class GenericBase(Generic[T]):
pass
print(get_origin(GenericBase[int]))
print(get_origin(GenericBase[str]) is GenericBase)
Output:
<class '__main__.GenericBase'>
True
5) Putting them together
With these components, we can now write a function get_type_arg
that takes a class as an argument and -- if that class is specialized form of our GenericBase
-- returns its type argument:
from typing import Generic, TypeVar, get_origin, get_args
T = TypeVar("T")
class GenericBase(Generic[T]):
pass
class Specific(GenericBase[int]):
pass
def get_type_arg(cls):
for base in cls.__orig_bases__:
origin = get_origin(base)
if origin is None or not issubclass(origin, GenericBase):
continue
return get_args(base)[0]
print(get_type_arg(Specific))
Output:
<class 'int'>
Now all that is left to do is embed this directly as a class-method of GenericBase
, optimize it a little bit and fix the type annotations.
One thing we can do to optimize this, is only run this algorithm only once for any given subclass of GenericBase
, namely when it is defined, and then save the type in a class-attribute. Since the type argument presumably never changes for a specific class, there is no need to compute this every time we want to access the type argument. To accomplish this, we can hook into __init_subclass__
and do our loop there.
We should also define a proper response for when get_type_arg
is called on a (unspecified) generic class. An AttributeError
seems appropriate.
6) Full working example
from typing import Any, Generic, Optional, Type, TypeVar, get_args, get_origin
# The `GenericBase` must be parameterized with exactly one type variable.
T = TypeVar("T")
class GenericBase(Generic[T]):
_type_arg: Optional[Type[T]] = None # set in specified subclasses
@classmethod
def __init_subclass__(cls, **kwargs: Any) -> None:
"""
Initializes a subclass of `GenericBase`.
Identifies the specified `GenericBase` among all base classes and
saves the provided type argument in the `_type_arg` class attribute
"""
super().__init_subclass__(**kwargs)
for base in cls.__orig_bases__: # type: ignore[attr-defined]
origin = get_origin(base)
if origin is None or not issubclass(origin, GenericBase):
continue
type_arg = get_args(base)[0]
# Do not set the attribute for GENERIC subclasses!
if not isinstance(type_arg, TypeVar):
cls._type_arg = type_arg
return
@classmethod
def get_type_arg(cls) -> Type[T]:
if cls._type_arg is None:
raise AttributeError(
f"{cls.__name__} is generic; type argument unspecified"
)
return cls._type_arg
def demo_a() -> None:
class SpecificA(GenericBase[int]):
pass
print(SpecificA.get_type_arg())
def demo_b() -> None:
class Foo:
pass
class Bar:
pass
class GenericSubclass(GenericBase[T]):
pass
class SpecificB(Foo, GenericSubclass[str], Bar):
pass
type_b = SpecificB.get_type_arg()
print(type_b)
e = type_b.lower("E") # static type checkers correctly infer `str` type
assert e == "e"
if __name__ == '__main__':
demo_a()
demo_b()
Output:
<class 'int'>
<class 'str'>
An IDE like PyCharm even provides the correct auto-suggestions for whatever type is returned by get_type_arg
, which is really nice. /p>
7) Caveats
- The
__orig_bases__
attribute is not well documented. I am not sure it should be considered entirely stable. Although it doesn't appear to be "just an implementation detail" either. I would suggest keeping an eye on that. mypy
seems to agree with this caution and raises ano attribute
error in the place where you access__orig_bases__
. Thus atype: ignore
was placed in that line.- The entire setup is for one single type parameter for our generic class. It can be adapted relatively easily to multiple parameters, though annotations for type checkers might become more tricky.
- This method does not work when called directly from a specialized
GenericBase
class, i.e.GenericBase[str].get_type_arg()
. But for that one just needs to calltyping.get_args
on it as shown in the very beginning.
Get generic type of class at runtime with Python 3.6
I have figured out how to do it. For some reason, __orig_bases__
wasn't working but after rebuilding my Python 3.6 and using a VENV, it works. This is the final working method:
py_version = sys.version_info
if py_version >= (3, 8):
from typing import get_args
def get_generic_type_arg(cls):
t = cls.__orig_bases__[0]
if py_version >= (3, 8):
return get_args(t)[0]
else:
return t.__args__[0]
Python 3.6 also requires to install dataclasses
as a library though it wasn't part of this issue. Also note that __args__
seems to be undocumented in Python 3.6 and removed from Python 3.8.
Get Type Argument of Arbitrarily High Generic Parent Class at Runtime
The following approach is based on __class_getitem__
and __init_subclass__
. It might serve your use case, but it has some severe limitations (see below), so use at your own judgement.
from __future__ import annotations
from typing import Generic, Sequence, TypeVar
T = TypeVar('T')
NO_ARG = object()
class Parent(Generic[T]):
arg = NO_ARG # using `arg` to store the current type argument
def __class_getitem__(cls, key):
if cls.arg is NO_ARG or cls.arg is T:
cls.arg = key
else:
try:
cls.arg = cls.arg[key]
except TypeError:
cls.arg = key
return super().__class_getitem__(key)
def __init_subclass__(cls):
if Parent.arg is not NO_ARG:
cls.arg, Parent.arg = Parent.arg, NO_ARG
class Child1(Parent[int]):
pass
class Child2(Child1):
pass
class Child3(Parent[T], Generic[T]):
pass
class Child4(Parent[Sequence[T]], Generic[T]):
pass
def get_parent_type_parameter(cls):
return cls.arg
classes = [
Parent[str],
Child1,
Child2,
Child3[int],
Child4[float],
]
for cls in classes:
print(cls, get_parent_type_parameter(cls))
Which outputs the following:
__main__.Parent[str] <class 'str'>
<class '__main__.Child1'> <class 'int'>
<class '__main__.Child2'> <class 'int'>
__main__.Child3[int] <class 'int'>
__main__.Child4[float] typing.Sequence[float]
This approach requires that every Parent[...]
(i.e. __class_getitem__
) is followed by an __init_subclass__
because otherwise the former information may be overwritten by a second Parent[...]
. For that reasons it won't work with type aliases for example. Consider the following:
classes = [
Parent[str],
Parent[int],
Parent[float],
]
for cls in classes:
print(cls, get_parent_type_parameter(cls))
which outputs:
__main__.Parent[str] <class 'float'>
__main__.Parent[int] <class 'float'>
__main__.Parent[float] <class 'float'>
Get generic substituted type
In the comments @alex_noname posted a link to another such question.
In my case the answer was to use the __orig_class__
attribute of the self
object:
from typing import Generic, TypeVar, get_args
from pydantic import BaseModel
T = TypeVar("T", bound=BaseModel)
class A(Generic[T]):
def some_method(self) -> T:
... # some data processing
return get_args(self.__orig_class__)[0].from_orm(some_data_model)
For more information please refer to this answer
Python Typing: how to get Type[C] to work with TypeVars and Generics?
Can you clarify what you mean by "Type[T] seems logical here, but that's not valid according to the docs"?
In particular, what docs are you looking at? The following code works as expected for me, using mypy 0.630:
class FooBarContainer(Generic[T]):
child: T
def __init__(self, ctorable: Type[T], val) -> None:
self.child = ctorable(val)
val = 3
baz = FooBarContainer(Foo, val)
# Mypy reports a `"Foo" has no attribute "barval"` error
failure = baz.child.barval
If the docs imply giving ctorable
a type of Type[T]
does not work, they should probably be updated.
Related Topics
How to Print a List with Integers Without the Brackets, Commas and No Quotes
Python Flask Intentional Empty Response
Error Installing Psycopg2 on MACos 10.9.5
Filtering a List Based on a List of Booleans
Test Case Execution Order in Pytest
Writing to a File in a for Loop Only Writes the Last Value
Changing User Agent in Python 3 for Urrlib.Request.Urlopen
How to Convert a Scikit-Learn Dataset to a Pandas Dataset
How to Change Foreignkey Display Text in the Django Admin
Download File Using Partial Download (Http)
Multiprocessing - Pipe VS Queue
How to Find the First Key in a Dictionary