Unpack Generic Types with get_origin and get_args
Python's typing module provides type hinting primarily
for static analysis, but libraries and frameworks often need to inspect
these types at runtime. The typing.get_origin() and
typing.get_args() functions, introduced in Python 3.8,
serve as the standard interface for unpacking parameterized generic
types and generic type aliases. Together, they allow developers to
dissect complex type annotations—such as list[str],
Union[int, float], or custom generic classes—into their
foundational types and constituent arguments.
Understanding Generic Type Decomposition
A parameterized type alias consists of two components:
- The Origin: The base or unsubscribed generic
class/constructor (e.g.,
listinlist[str]). - The Arguments: The type parameters supplied inside
the brackets as a tuple (e.g.,
(str,)inlist[str]).
Directly inspecting these structures using type() or
isinstance() is unreliable because parameterized generics
are instances of internal typing constructs (such as
types.GenericAlias or typing._GenericAlias).
The functions get_origin() and get_args()
abstract away internal implementation details to return predictable
values.
How typing.get_origin()
Works
get_origin(tp) takes a type annotation tp
and returns the unsubscribed version of the type. If the annotation is
not a parameterized generic or a supported typing construct, it returns
None.
from typing import get_origin, Union, Literal
# Standard collections and typing wrappers
print(get_origin(list[int])) # Output: <class 'list'>
print(get_origin(dict[str, float])) # Output: <class 'dict'>
# Special typing constructs
print(get_origin(Union[int, str])) # Output: typing.Union
print(get_origin(int | str)) # Output: typing.Union (or types.UnionType)
print(get_origin(Literal["a", "b"])) # Output: typing.Literal
# Non-parameterized types return None
print(get_origin(int)) # Output: None
print(get_origin(list)) # Output: NoneHow typing.get_args()
Works
get_args(tp) extracts the generic arguments passed to
tp and returns them as a tuple. If
tp is not parameterized, it returns an empty tuple
().
from typing import get_args, Union, Callable
# Unpacking arguments
print(get_args(list[int])) # Output: (<class 'int'>,)
print(get_args(dict[str, float])) # Output: (<class 'str'>, <class 'float'>)
print(get_args(Union[int, str])) # Output: (<class 'int'>, <class 'str'>)
# Callable types: arguments are represented as a list or ellipsis, followed by return type
print(get_args(Callable[[str, int], bool])) # Output: ([<class 'str'>, <class 'int'>], <class 'bool'>)
# Non-parameterized types return an empty tuple
print(get_args(int)) # Output: ()
print(get_args(list)) # Output: ()Unpacking Custom Generic Classes
Custom generic types created using typing.Generic or
Python 3.12+ generic syntax (PEP 695) follow the same decomposition
rules.
from typing import Generic, TypeVar, get_origin, get_args
T = TypeVar("T")
class Container(Generic[T]):
def __init__(self, value: T):
self.value = value
Alias = Container[int]
print(get_origin(Alias)) # Output: <class '__main__.Container'>
print(get_args(Alias)) # Output: (<class 'int'>,)Unpacking Nested Generics Recursively
get_origin() and get_args() operate
shallowly; they inspect only the outermost layer of a generic. To
inspect deeply nested type declarations, recursively evaluate each
argument:
from typing import get_origin, get_args
def unpack_type(tp):
origin = get_origin(tp)
args = get_args(tp)
if origin is None:
return {"base": tp, "args": ()}
return {
"origin": origin,
"args": [unpack_type(arg) for arg in args]
}
nested_type = dict[str, list[int]]
print(unpack_type(nested_type))
# Output:
# {
# 'origin': <class 'dict'>,
# 'args': [
# {'base': <class 'str'>, 'args': ()},
# {'origin': <class 'list'>, 'args': [{'base': <class 'int'>, 'args': ()}]}
# ]
# }Key Edge Cases
Optional[T]: SinceOptional[T]is syntactic sugar forUnion[T, None],get_origin(Optional[int])returnstyping.Union, andget_args(Optional[int])returns(<class 'int'>, <class 'NoneType'>).Tuplewithout fixed size: A variable-length tuple annotation liketuple[int, ...]returns(<class 'int'>, Ellipsis)viaget_args().- Empty generics: Subscribing without types (e.g.,
Tuple[()]) returns()inget_args().