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:

  1. The Origin: The base or unsubscribed generic class/constructor (e.g., list in list[str]).
  2. The Arguments: The type parameters supplied inside the brackets as a tuple (e.g., (str,) in list[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: None

How 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