What's the correct way to check if an object is a typing.Generic?

Question:

I’m trying to write code that validates type hints, and in order to do so I have to find out what kind of object the annotation is. For example, consider this snippet that’s supposed to tell the user what kind of value is expected:

import typing

typ = typing.Union[int, str]

if issubclass(typ, typing.Union):
    print('value type should be one of', typ.__args__)
elif issubclass(typ, typing.Generic):
    print('value type should be a structure of', typ.__args__[0])
else:
    print('value type should be', typ)

This should print “value type should be one of (int, str)”, but instead it throws an exception:

Traceback (most recent call last):
  File "untitled.py", line 6, in <module>
    if issubclass(typ, typing.Union):
  File "C:Python34libsite-packagestyping.py", line 829, in __subclasscheck__
    raise TypeError("Unions cannot be used with issubclass().")
TypeError: Unions cannot be used with issubclass().

isinstance doesn’t work either:

>>> isinstance(typ, typing.Union)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:Python34libsite-packagestyping.py", line 826, in __instancecheck__
    raise TypeError("Unions cannot be used with isinstance().")
TypeError: Unions cannot be used with isinstance().

What’s the correct way to check if typ is a typing.Generic?

If possible, I would like to see a solution that’s backed by documentation or a PEP or some other resource. A “solution” that “works” by accessing undocumented, internal attributes is easy to find. But more likely than not, it’ll turn out to be an implementation detail and will change in future versions. I’m looking for “the right way” to do it.

Asked By: Aran-Fey

||

Answers:

You may be looking for __origin__:

# * __origin__ keeps a reference to a type that was subscripted,
#   e.g., Union[T, int].__origin__ == Union;`

import typing

typ = typing.Union[int, str]

if typ.__origin__ is typing.Union:
    print('value type should be one of', typ.__args__)
elif typ.__origin__ is typing.Generic:
    print('value type should be a structure of', typ.__args__[0])
else:
    print('value type should be', typ)

>>>value type should be one of (<class 'int'>, <class 'str'>)

The best I could find to advocate the use of this undocumented attribute is this reassuring quote from Guido Van Rossum (dated 2016):

The best I can recommend is using __origin__ — if we were to change this attribute there would still have to be some other way to access the same information, and it would be easy to grep your code for occurrences of __origin__. (I’d be less worried about changes to __origin__ than to __extra__.) You may also look at the internal functions _gorg() and _geqv() (these names will not be part of any public API, obviously, but their implementations are very simple and conceptually useful).

This caveat in the documentation seem to indicate that nothing is set in marble yet:

New features might be added and API may change even between minor releases if deemed necessary by the core developers.

Answered By: Jacques Gaudin

The most you could do, I think, is to use your typ on a variable, use typing.get_type_hints on it and extract the info you need from the returned __annotations__-like dictionary.

PEP-484 says:

get_type_hints(), a utility function to retrieve the type hints from a function or method. Given a function or method object, it returns a dict with the same format as __annotations__, but evaluating forward references (which are given as string literals) as expressions in the context of the original function or method definition.

26.1.7. Classes, functions, and decorators says:

At runtime, isinstance(x, T) will raise TypeError. In general, isinstance() and issubclass() should not be used with types.

However, PEP-526 says in ‘Non-goals’:

While the proposal is accompanied by an extension of the typing.get_type_hints standard library function for runtime retrieval of annotations, variable annotations are not designed for runtime type checking. Third party packages will have to be developed to implement such functionality.

Answered By: edixon

There is no official way to obtain this information. The typing module is still in heavy development, and has no public API to speak of. (In fact, it will probably never have one.)

All we can do is to look at the module’s internals and find the least gross way to get the information we’re after. And because the module is still being worked on, its internals will change. A lot.


In python 3.5 and 3.6, generics had an __origin__ attribute that held a reference to the original generic base class (i.e. List[int].__origin__ would’ve been List), but this was changed in 3.7. Now the easiest way to find out if something is a generic is probably to check its __parameters__ and __args__ attributes.

Here is a set of functions that can be used to detect generics:

import typing


if hasattr(typing, '_GenericAlias'):
    # python 3.7
    def _is_generic(cls):
        if isinstance(cls, typing._GenericAlias):
            return True

        if isinstance(cls, typing._SpecialForm):
            return cls not in {typing.Any}

        return False


    def _is_base_generic(cls):
        if isinstance(cls, typing._GenericAlias):
            if cls.__origin__ in {typing.Generic, typing._Protocol}:
                return False

            if isinstance(cls, typing._VariadicGenericAlias):
                return True

            return len(cls.__parameters__) > 0

        if isinstance(cls, typing._SpecialForm):
            return cls._name in {'ClassVar', 'Union', 'Optional'}

        return False
else:
    # python <3.7
    if hasattr(typing, '_Union'):
        # python 3.6
        def _is_generic(cls):
            if isinstance(cls, (typing.GenericMeta, typing._Union, typing._Optional, typing._ClassVar)):
                return True

            return False


        def _is_base_generic(cls):
            if isinstance(cls, (typing.GenericMeta, typing._Union)):
                return cls.__args__ in {None, ()}

            if isinstance(cls, typing._Optional):
                return True

            return False
    else:
        # python 3.5
        def _is_generic(cls):
            if isinstance(cls, (typing.GenericMeta, typing.UnionMeta, typing.OptionalMeta, typing.CallableMeta, typing.TupleMeta)):
                return True

            return False


        def _is_base_generic(cls):
            if isinstance(cls, typing.GenericMeta):
                return all(isinstance(arg, typing.TypeVar) for arg in cls.__parameters__)

            if isinstance(cls, typing.UnionMeta):
                return cls.__union_params__ is None

            if isinstance(cls, typing.TupleMeta):
                return cls.__tuple_params__ is None

            if isinstance(cls, typing.CallableMeta):
                return cls.__args__ is None

            if isinstance(cls, typing.OptionalMeta):
                return True

            return False


def is_generic(cls):
    """
    Detects any kind of generic, for example `List` or `List[int]`. This includes "special" types like
    Union and Tuple - anything that's subscriptable, basically.
    """
    return _is_generic(cls)


def is_base_generic(cls):
    """
    Detects generic base classes, for example `List` (but not `List[int]`)
    """
    return _is_base_generic(cls)


def is_qualified_generic(cls):
    """
    Detects generics with arguments, for example `List[int]` (but not `List`)
    """
    return is_generic(cls) and not is_base_generic(cls)

All of these functions should work in all python versions <= 3.7 (including anything <3.5 that uses the typing module backport).

Answered By: Aran-Fey

As pointed out by sonny-garcia in the comments, get_origin() works from python 3.8

import typing
from typing import get_origin

typ = typing.Union[int, str]
get_origin(typ) == typing.Union
#True

You can find more details in the docs

Answered By: saintlyzero

I programmed this in one of my projects:

def is_type(arg: object) -> bool:
    """Return True if `arg` is a type, including those for generic typing."""
    from typing import Any, get_origin

    generic_base = type(Any)
    if (aux := generic_base.__base__) is not object:
        generic_base = aux
    return isinstance(arg, (type, generic_base)) or get_origin(arg)
Answered By: Medardo Rodriguez

As of Python 3.8 typing.getOrigin(tp) is the proper way

The docstring is thoroughly instructive (Notably returns None if not from typing):

def get_origin(tp):
    """Get the unsubscripted version of a type.

    This supports generic types, Callable, Tuple, Union, Literal, Final, ClassVar
    and Annotated. Return None for unsupported types. Examples::

        get_origin(Literal[42]) is Literal
        get_origin(int) is None
        get_origin(ClassVar[int]) is ClassVar
        get_origin(Generic) is Generic
        get_origin(Generic[T]) is Generic
        get_origin(Union[T, int]) is Union
        get_origin(List[Tuple[T, T]][int]) == list
    """

In your use case it would be something like:

import typing

def func(typ):
    if typing.get_origin(typ) is typing.Union:
        print('value type should be one of', typing.get_args(typ))
    elif typing.get_origin(typ) is typing.Generic:
        print('value type should be a structure of', typing.get_args(typ))
    else:
        print('value type should be', typ)

A = typing.TypeVar("A")
B = typing.TypeVar("B")

func(typing.Union[int, str])
func(typing.Generic[A, B])
func(int)

>>> "value type should be one of (<class 'int'>, <class 'str'>)"
>>> "value type should be a structure of (~A, ~B)"
>>> "value type should be <class 'int'>"
Answered By: Mandera
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.