Generic[T] base class - how to get type of T from within instance?
Asked Answered
N

7

58

Assume you have a Python class that inherits from Generic[T]. Is there any way to get a hold of the actual type passed in from within the class/instance?

For example,

from typing import TypeVar, Type, Generic
T = TypeVar('T')

class Test(Generic[T]):
    def hello(self):
      my_type = T  # this is wrong!
      print( "I am {0}".format(my_type) )

Test[int]().hello() # should print "I am int"

On here, it is suggested the type arg would be present in in the args field of the type. And indeed,

print( str( Test[int].__args__ ) )

would print (<class 'int'>,). However, I can't seem to access this from within the instance directly, e.g. substituting

      my_type = self.__class__.__args__ # this is also wrong (None)

doesn't seem to to the trick.

Thanks

Necropsy answered 29/8, 2019 at 8:32 Comment(2)
Do you need to use the type method? e.g. my_type = type(T)?Aparri
Unfortunately not, type(T) would be typing.TypeVarNecropsy
N
50

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.

Netherlands answered 2/4, 2020 at 5:20 Comment(4)
This does answer the question. My remaining inquiry is: At what point in the object's lifecycle does __orig_clas__ become available? I would like to access that information during __init__, but from what I have tried so far, it does not appear to be available, even after calling super().__init__(). Obviously, that is appropriate for a separate question.Absolve
@CarterPape: Depends on your Python version, but currently, it's set in typing._GenericAlias.__call__, after normal object construction is already complete and all __new__ and __init__ methods have finished executing.Netherlands
Yeah I just sifted through that to find where it's set, thanks for pointing it out. By what black magic does __call__ get invoked? Also, I've created a separate question to take the discussion.Absolve
but orig_clas is not documented feature, thus a hackMahaliamahan
L
18

Currently (Python 3.10.3) one cannot access the type parameter during __init__ or __new__.

However, it's possible to access the type variable in __init_subclass__. It's a bit different scenario, but I think it's interesting enough to share.

from typing import Any, Generic, TypeVar, get_args

T = TypeVar("T")


class MyGenericClass(Generic[T]):
    _type_T: Any

    def __init_subclass__(cls) -> None:
        cls._type_T = get_args(cls.__orig_bases__[0])[0]  # type: ignore


class SomeBaseClass(MyGenericClass[int]):
    def __init__(self) -> None:
        print(self._type_T)


SomeBaseClass()  # prints "<class 'int'>"
Linin answered 2/4, 2022 at 18:55 Comment(1)
Definitely interesting enough to share!Protestantism
E
12

This is possible if you're willing to adjust the syntax of how your class is instantiated a little bit.

import typing

T = typing.TypeVar('T')
class MyGenericClass(typing.Generic[T]):
    def __init__(self, generic_arg: typing.Type[T]) -> None:
        self._generic_arg = generic_arg

    def print_generic_arg(self) -> None:
        print(f"My generic argument is {self._generic_arg}")

    def print_value(self, value: T) -> None:
        print(value)

my_instance = MyGenericClass(int)  # Note, NOT MyGenericClass[int]().

my_instance.print_generic_arg()  # Prints "My generic argument is <class 'int'>".

reveal_type(my_instance)  # Revealed type is MyGenericClass[builtins.int*].
                          # Note the asterisk, indicating that 
                          # builtins.int was *inferred.*

my_instance.print_value(123)  # OK, prints 123.
my_instance.print_value("abc")  # Type-checking error, "abc" isn't an int.

As one of the other answers here explains, trying to retrieve the type argument at runtime from the instance's type can be inherently problematic. In part, because the instance wasn't necessarily even created with a type argument to begin with.

So, this approach works around that by coming at the problem the other way. We require the caller to pass the type as an argument to __init__. Then, instead of trying to derive a runtime value of int from the instance's type MyGenericClass[int], we start with the runtime value of int and let the type checker infer that the instance's type is MyGenericClass[int].

In more complex cases, you might need to help out the type checker by redundantly specifying the type. It will be your responsibility to make sure the types match.

possible_types = {"int": int, "str": str}

# Error: "Need type annotation".
my_instance = MyGenericClass(possible_types["int"])

# OK.
my_instance = MyGenericClass[int](possible_types["int"])
Edaphic answered 5/12, 2021 at 2:14 Comment(2)
it can combine with __new__ that could return arbitrary type, while __init__ can only return the instance itself python class PatternArg(Generic[GT_PatternArg]): def __new__( cls, type_: Type[GT_PatternArg], default: Optional[GT_PatternArg] = None ): self.type_ = type_ self.default = default return cast(GT_PatternArg, "PatternArg") Ptarmigan
I made a gist which uses this pattern, it was a useful exercise for me. Sharing just in case it's useful for others: gist.github.com/MatrixManAtYrService/…Dextrin
E
8

you can use self.__orig_class__:

from typing import TypeVar, Type, Generic
T = TypeVar('T')

class Test(Generic[T]):

    def hello(self):
        print( "I am {0}".format(self.__orig_class__.__args__[0].__name__))

Test[int]().hello()
# I am int
Ecbatana answered 6/4, 2020 at 10:56 Comment(1)
if no generic specified then AttributeError: 'Test' object has no attribute 'orig_class' . Also orig_class is not documented thus this is an specific implementation hack solution.Mahaliamahan
C
4

PEP560 introduced cls.__orig_bases__ and the typing module has the utility functions get_origin and get_args that allow you to extract the type; however, when you have a (complex) class hierarchy with multiple levels of generic inheritance then you need to iterate over that hierarchy and push the concrete type parameters down through the hierarchy.

For example, if you have the classes:

from typing import Generic, Literal, LiteralString
from datetime import date

T = TypeVar("T")
L = TypeVar("L", bound=LiteralString)
F = TypeVar("F", bound=str)

class Thing(Generic[T, L]):
    pass

class ChildThing(Thing[T, L], Generic[T, L]):
    pass

class StringThing(ChildThing[T, Literal["string"]], Generic[F, T]):
    pass

class DateThing(StringThing[Literal["date"], date]):
    pass

Then:

print(get_args(DateThing.__orig_bases__[0]))

Will output:

(typing.Literal['date'], <class 'datetime.date'>)

And will not give you the type of the parameter T for the base Thing class. For that you need more complicated logic:

from typing import (
    Any,
    Dict,
    Generic,
    Literal,
    LiteralString,
    Optional,
    Tuple,
    TypeVar,
    get_args,
    get_origin,
)
from datetime import date

T = TypeVar("T")
L = TypeVar("L", bound=LiteralString)
F = TypeVar("F", bound=str)

def get_generic_map(
    base_cls: type,
    instance_cls: type,
) -> Dict[Any, Any]:
    """Get a map from the generic type paramters to the non-generic implemented types.

    Args:
        base_cls: The generic base class.
        instance_cls: The non-generic class that inherits from ``base_cls``.

    Returns:
        A dictionary mapping the generic type parameters of the base class to the
        types of the non-generic sub-class.
    """
    assert base_cls != instance_cls
    assert issubclass(instance_cls, base_cls)
    cls: Optional[type] = instance_cls
    generic_params: Tuple[Any, ...]
    generic_values: Tuple[Any, ...] = tuple()
    generic_map: Dict[Any, Any] = {}

    # Iterate over the class hierarchy from the instance sub-class back to the base
    # class and push the non-generic type paramters up through that hierarchy.
    while cls is not None and issubclass(cls, base_cls):
        if hasattr(cls, "__orig_bases__"):
            # Generic class
            bases = cls.__orig_bases__

            # Get the generic type parameters.
            generic_params = next(
                (
                    get_args(generic)
                    for generic in bases
                    if get_origin(generic) is Generic
                ),
                tuple(),
            )

            # Generate a map from the type parameters to the non-generic types pushed
            # from the previous sub-class in the hierarchy.
            generic_map = (
                {param: value for param, value in zip(generic_params, generic_values)}
                if len(generic_params) > 0
                else {}
            )

            # Use the type map to push the concrete parameter types up to the next level
            # of the class hierarchy.
            generic_values = next(
                (
                    tuple(
                        generic_map[arg] if arg in generic_map else arg
                        for arg in get_args(base)
                    )
                    for base in bases
                    if (
                        isinstance(get_origin(base), type)
                        and issubclass(get_origin(base), base_cls)
                    )
                ),
                tuple(),
            )
        else:
            generic_map = {}

        assert isinstance(cls, type)
        cls = next(
            (base for base in cls.__bases__ if issubclass(base, base_cls)),
            None,
        )

    return generic_map


class Thing(Generic[T, L]):
    type: T
    literal: L
    def __init__(self) -> None:
        super().__init__()
        type_map = get_generic_map(Thing, type(self))
        self.type = type_map[T]
        self.literal = type_map[L]

    def hello(self) -> str:
        return f"I am type={self.type}, literal={self.literal}"

class ChildThing(Thing[T, L], Generic[T, L]):
    pass

class StringThing(ChildThing[T, Literal["string"]], Generic[F, T]):
    def __init__(self) -> None:
        super().__init__()
        type_map = get_generic_map(StringThing, type(self))
        self.f = type_map[F]

    def hello(self) -> str:
        return f"{super().hello()}, f={self.f}"

class DateThing(StringThing[Literal["date"], date]):
    pass

thing = DateThing()
print(thing.hello())

Which outputs:

I am type=<class 'datetime.date'>, literal=typing.Literal['string'], f=typing.Literal['date']
Conundrum answered 15/5, 2023 at 0:28 Comment(0)
K
0

I know this is an old question. But for guys who can't overwrite the __class_get_item__ function it can be a bit tedious to work through these undocumented variables. Since then the __orig_classes__ (and other) fields are still present, even when using the new PEP 695 syntax in Python 3.12. So I decided to create a little tool to do the introspection for you using these undocumented variables: python-generics

Currently, it doesn't work during instantiation (i.e. inside the __init__ method with the self object) but with enough demand I'm willing to implement this feature as well.

With your example you'll be able to simply write something like:

from typing import TypeVar, Type, Generic
from generics import get_filled_type

T = TypeVar('T')

class Test(Generic[T]):
    def hello(self):
      my_type = get_filled_type(self, Test, T)
      print( "I am {0}".format(my_type) )

Test[int]().hello() # should print "I am int"
Kooima answered 23/7, 2024 at 22:0 Comment(0)
V
-2

You can use something else entirely.

# Define Wrapper Class
class Class():
    # Define __getitem__ method to be able to use index
    def __getitem__(self, type):
        # Define Main Class
        class Class():
            __doc__ = f"""I am an {type.__name__} class"""

            def __init__(self, value):
                self.value: type = type(value)
        # Return Class
        return Class
# Set Class to an instance of itself to be able to use the indexing
Class = Class()

print(Class[int].__doc__)
print(Class[int](5.3).value)

Use this if you want. You can use the type variable throughout the whole class, even when not using self. Just saying, syntax highlighters might have a hard time understanding this sort of thing because its using a return of something which they don't really care about, even if its a class. At least for pylance, because that's what I use.

Vend answered 11/7, 2021 at 8:50 Comment(3)
This is exactly the solution I wantedSubversive
I don't understand re-defining Class inside __getitem__ - that doesn't feel like good practiceAlkalosis
This conflates the use of a generic with the runtime __getitem__ dunder, which is to say it doesn't use type annotations at all. Even if the output is sorta what the OP wants in their minimum viable example, it isn't produced in a way that addresses the actual question.Protestantism

© 2022 - 2025 — McMap. All rights reserved.