Python typing system concepts

September 28, 2026

Modern Python programming provides rich typing system to inform the type checkers about static type information. As the typing system envolves, there are more and more comprehensive concepts that needs further understandings.

Generics

Suppose we have the following Python snippet with type annotated:

from typing import Generic, TypeVar

T = TypeVar("T")

class Box(Generic[T]):  # or class Box[T]: with Python 3.12+ with TypeVar declaration omitted
    def __init__(self, value: T):
        self.value = value

def consume(box: Box[int]) -> None:
    pass

There are some concepts with the above code:

  • Generic: Box, with unresolved and variable type parameters (T) during type declaration
  • type variable: T, used as a substitutable type
  • type parameter: T in the Box scope, like placeholder 1
  • type argument: int in Box[int]
  • parameterization: Box[int], substitute the type parameters with real types

Then, the type of a parameterized Generic will be:

print(type(Box[int]))
<class 'typing._GenericAlias'>
☠️

There is another similar type named types.GenericAlias, which has almost the same functionality with typing._GenericAlias. It serves to Python built-in generic types like list[int].

Loading…

Notably, CPython will NOT specialize a dedicated type for each generic alias, the object is still an instance of the "original" type:

print(type(Box[str]("123")))
<class '__main__.Box'>

Generic Alias

With parameterized types, Generic now becomes to _GenericAlias, bound to some important attributes allowing inspect code or type checker to access type info:

Object__origin____args____parameters__
Box[T]Box(T,)(T,)
Box[int]Box(int,)()
Box[list[T]]Box(list[T],)(T,)
list[int]list(int,)()

To distinguish the type parameter declared by Box from the free type parameter in its argument, we redefine Box with U:

from typing import Generic, TypeVar

U = TypeVar("U")
T = TypeVar("T")

class Box(Generic[U]):
    pass

A = Box[list[T]]
B = A[int]

print(A.__origin__, A.__parameters__, A.__args__)
print(B.__origin__, B.__parameters__, B.__args__)

# <class '__main__.Box'> (~T,) (list[~T],)
# <class '__main__.Box'> () (list[int],)

Nested generic aliases: parameter collection during construction and substitution during reparameterization.

TypeVarTuple and ParamSpec

There are two kinds of special types used for function signature and forwarding/wrapping type hints: TypeVarTuple and ParamSpec.

Let's consider the following case:

def pack[*Ts](*args: *Ts) -> tuple[*Ts]:
    return args

Ts receives a series of TypeVars, where the order and types of each position are preserved and could be detected by type checker.

Additionally, ParamSpec could be further used to record the positional-only and keyword-only information during the function forwarding or wrapping:

def logged[**P, R](func: Callable[P, R]) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print("calling", func.__name__)
        return func(*args, **kwargs)

    return wrapper

The construction of TypeVarTuple and ParamSpec are located in:

Loading…

This utility function helps converting and resolving the case class C[A, *Ts, B]: ... with args C[int, str, bool, bytes]. It will wrap the flatten args into (int, (str, bool), bytes).

As for ParamSpec, typing lib is responsible for validating acceptable args and replace into the default values whenever needed.

Loading…

Footnotes

  1. There is another dunder attribute __type_param__ used to obtain the type parameters specified by new grammer [T]. ↩