TypeVar and Generic Programming

Difficulty: Advanced

Question

What are TypeVar and Generic in Python type hints? How do you write generic functions and classes?

Answer

TypeVar creates a placeholder type variable that is resolved when a function or class is used.

Generic functions: the return type relates to the input type (e.g., first(list[int]) -> int). Generic classes: parameterize the class with a type (Stack[int], Queue[str]).

In Python 3.12+: generic syntax is simplified - def first[T](lst: list[T]) -> T instead of TypeVar.

Bound TypeVar: restricts acceptable types to a base class. Constrained TypeVar: only the listed types are accepted.

Code examples

TypeVar and Generic Functions

from typing import TypeVar, List, Optional, Sequence

T = TypeVar('T')  # Unrestricted
Numeric = TypeVar('Numeric', int, float)  # Constrained to int or float

# Generic function: return type matches input type
def first(seq: Sequence[T]) -> Optional[T]:
    return seq[0] if seq else None

def last(seq: Sequence[T]) -> Optional[T]:
    return seq[-1] if seq else None

def repeat(item: T, times: int) -> List[T]:
    return [item] * times

# Type checker knows: first([1, 2, 3]) returns int, not Any
result_int = first([1, 2, 3])      # int
result_str = first(['a', 'b'])    # str
result_none = first([])            # None

print(first([1, 2, 3]))   # 1
print(first(['a', 'b']))  # a
print(repeat('x', 3))     # ['x', 'x', 'x']

# Bound TypeVar
from typing import TypeVar
Comparable = TypeVar('Comparable', bound='Comparable')

# Built-in: Comparable constraint
def maximum(a: Numeric, b: Numeric) -> Numeric:
    return a if a > b else b

print(maximum(3, 5))      # 5
print(maximum(3.14, 2.0)) # 3.14

TypeVar T is inferred from the input type. The type checker knows first([1, 2, 3]) returns int, enabling better error detection.

Generic Classes

from typing import TypeVar, Generic, Iterator

T = TypeVar('T')

class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []

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

    def pop(self) -> T:
        if not self._items:
            raise IndexError('Stack is empty')
        return self._items.pop()

    def peek(self) -> T:
        return self._items[-1]

    def __len__(self) -> int:
        return len(self._items)

    def __iter__(self) -> Iterator[T]:
        return reversed(self._items)

# Type-safe usage
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
print(int_stack.pop())  # 2 - type checker knows this is int

str_stack: Stack[str] = Stack()
str_stack.push('hello')
print(str_stack.peek())  # hello

# Python 3.12+ simplified syntax
# def first[T](seq: list[T]) -> T: ...
# class Stack[T]:
#     def push(self, item: T) -> None: ...

Stack[int] tells the type checker that pop() returns int, not Any. The generic class works for any type - type safety comes from the annotation.

Key points

Concepts covered

TypeVar, Generic, Generics, Type Safety, mypy