Generic Types and TypeVar in Python

· 5 min read · Updated March 14, 2026 · advanced
python typing generics advanced

Generic types let you write code that works with multiple data types while still maintaining type safety. Instead of rewriting the same logic for list[int], list[str], or list[dict], you can define a single generic class or function that works with any type.

Why Generics Matter

Consider a simple stack implementation without generics:

class Stack:
    def __init__(self):
        self.items = []

    def push(self, item):
        self.items.append(item)

    def pop(self):
        return self.items.pop()

This works, but callers lose all type information. If you push integers, you get back Any when you pop. A generic stack preserves the type:

from typing import Generic, TypeVar

T = TypeVar('T')

class Stack(Generic[T]):
    def __init__(self):
        self.items: list[T] = []

    def push(self, item: T) -> None:
        self.items.append(item)

    def pop(self) -> T:
        return self.items.pop()

Now Stack[int]() gives you a stack that only accepts and returns integers, and type checkers will catch mistakes like pushing a string into an integer stack.

Defining TypeVar

TypeVar creates a type variable that represents some unspecified type:

from typing import TypeVar

T = TypeVar('T')

You can also constrain a TypeVar to a specific set of types:

from typing import TypeVar

Numeric = TypeVar('Numeric', int, float)

This is useful when your code needs to work with related types but not just any type. A function using this Numeric TypeVar can accept both ints and floats but nothing else.

Generic Functions

TypeVar shines in generic functions that operate on containers of any type:

from typing import TypeVar, Sequence

T = TypeVar('T')

def first(items: Sequence[T]) -> T:
    """Return the first item from a sequence."""
    return items[0]

def last(items: Sequence[T]) -> T:
    """Return the last item from a sequence."""
    return items[-1]

def reverse(items: Sequence[T]) -> list[T]:
    """Return a reversed copy of the sequence."""
    return list(reversed(items))

When you call first([1, 2, 3]), Python infers T as int. When you call first(['a', 'b']), it infers T as str. The same function works with any sequence type while preserving type information.

Generic Classes

Classes can be generic by inheriting from Generic[T]:

from typing import Generic, TypeVar

T = TypeVar('T')

class Box(Generic[T]):
    def __init__(self, content: T):
        self.content = content

    def get(self) -> T:
        return self.content

    def set(self, content: T) -> None:
        self.content = content

Usage:

string_box: Box[str] = Box("hello")
int_box: Box[int] = Box(42)

content: str = string_box.get()  # type: str

Multiple TypeVars

A class can use multiple TypeVars for different type parameters:

from typing import Generic, TypeVar

K = TypeVar('K')
V = TypeVar('V')

class Mapping(Generic[K, V]):
    def __init__(self):
        self._data: dict[K, V] = {}

    def set(self, key: K, value: V) -> None:
        self._data[key] = value

    def get(self, key: K) -> V:
        return self._data[key]

Now you can create Mapping[str, int], Mapping[int, str], or any combination you need.

Generic Constraints

Sometimes you need to restrict what types can be used. A bounded TypeVar requires the type to be a subclass of something:

from typing import TypeVar

Shape = TypeVar('Shape', bound='Shape')

class Shape:
    def area(self) -> float:
        raise NotImplementedError

class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def area(self) -> float:
        return 3.14159 * self.radius ** 2

class Square(Shape):
    def __init__(self, side: float):
        self.side = side

    def area(self) -> float:
        return self.side ** 2

With a bounded TypeVar, you can write functions that accept any Shape subclass while keeping the return type as the specific subclass:

def largest_area(shapes: list[Shape]) -> float:
    return max(shape.area() for shape in shapes)

TypeVar in the Standard Library

Many parts of Python’s standard library use generics. The typing module itself is generic:

from typing import List, Dict, Optional, Union

# These are all generic aliases
names: List[str] = []
scores: Dict[str, int] = []
user_id: Optional[int] = None
result: Union[int, str] = ""

Python 3.9 introduced the ability to use built-in types directly as generics:

# Python 3.9+ syntax
names: list[str] = []
scores: dict[str, int] = {}

Both forms are valid. The built-in syntax is preferred for Python 3.9+, while the typing module versions work on earlier versions.

Generic Protocols

Combine Protocol with generics for structural typing with type parameters:

from typing import Protocol, TypeVar

T = TypeVar('T')

class Container(Protocol[T]):
    def get(self) -> T: ...
    def set(self, value: T) -> None: ...

class IntContainer:
    def get(self) -> int:
        return 42

    def set(self, value: int) -> None:
        print(f"Setting {value}")

def process(container: Container[int]) -> None:
    value = container.get()
    container.set(value + 1)

process(IntContainer())  # Works because IntContainer matches Container[int]

This lets you define interfaces that any class can implement without explicit inheritance.

Practical Examples

Caching Decorator

from typing import TypeVar, Callable, Generic
from functools import lru_cache

T = TypeVar('T')

def cached(func: Callable[..., T]) -> Callable[..., T]:
    return lru_cache(maxsize=128)(func)

@cached
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

Generic Data Pipeline

from typing import TypeVar, Generic, Callable, Iterator

T = TypeVar('T')
R = TypeVar('R')

class Pipeline(Generic[T, R]):
    def __init__(self, initial: Iterator[T]):
        self.data = initial

    def map(self, func: Callable[[T], R]) -> 'Pipeline[T, R]':
        self.data = map(func, self.data)
        return self

    def filter(self, pred: Callable[[R], bool]) -> 'Pipeline[T, R]':
        self.data = filter(pred, self.data)
        return self

    def collect(self) -> list[R]:
        return list(self.data)

Common Patterns and Pitfalls

Do not overuse generics. If your function truly does not care about the type, Any might be clearer than an unbounded TypeVar. Generics add complexity—use them when they provide real value.

Remember that TypeVars are scoped to module level in most cases. Defining them inside a class or function can lead to unexpected behavior.

Python generics are checked at annotation time, not runtime. A list[int] is still a list at runtime. The type information exists for tools like mypy and pyright, not for runtime type enforcement.

See Also