How to Access the Type Arguments of Typing.Generic

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 the typing_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 a no attribute error in the place where you access __orig_bases__. Thus a type: 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 call typing.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



Leave a reply



Submit