Python's generics system brings type safety to dynamic code, enabling reusable functions and classes that work across types while aiding static analysis tools like mypy. Introduced in Python 3.5 through PEP 483 and refined in versions like 3.12, generics use type variables without runtime overhead, leveraging duck typing for flexibility.

What Are Generics?

Generics parameterize types, allowing structures like lists or custom classes to specify element types at usage time. Core building block: TypeVar from typing (built-in since 3.12). They exist purely for static checking—no enforcement at runtime, unlike Java's generics.

from typing import TypeVar
T = TypeVar('T')  # Placeholder for any type

Generic Functions in Action

Create flexible utilities by annotating parameters and returns with type variables. A practical example is a universal adder for any comparable types.

from typing import TypeVar

T = TypeVar('T')  # Any type supporting +

def add(a: T, b: T) -> T:
    return a + b

# Usage
result1: int = add(5, 3)           # Returns 8, type int
result2: str = add("hello", "world")  # Returns "helloworld", type str
result3: float = add(2.5, 1.7)     # Returns 4.2, type float

Mypy infers and enforces matching types—add(1, "a") fails checking. Another example: identity function.

def identity(value: T) -> T:
    return value

This works seamlessly across any type.

Building Generic Classes

Inherit from Generic[T] for type-aware containers (or use class Stack[T]: in 3.12+). A real-world Result type handles success/error cases like Rust's Result<T, E>.

from typing import Generic, TypeVar

T = TypeVar('T')  # Success type
E = TypeVar('E')  # Error type

class Result(Generic[T, E]):
    def __init__(self, value: T | None = None, error: E | None = None):
        self.value = value
        self.error = error
        self.is_ok = error is None

    def unwrap(self) -> T | None:
        if self.is_ok:
            return self.value
        raise ValueError(f"Error: {self.error}")
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:
        return self.items.pop()

Sample Usage:

# Result usage
def divide(a: float, b: float) -> Result[float, str]:
    if b == 0:
        return Result(error="Division by zero")
    return Result(value=a / b)

success = divide(10, 2)
print(success.unwrap())  # 5.0

failure = divide(10, 0)
# failure.unwrap() #raises ValueError

# Stack usage
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(42)
print(int_stack.pop())  # 42

str_stack: Stack[str] = Stack()
str_stack.push("hello")
print(str_stack.pop())  # "hello"

Advanced Features

  • Multiple TypeVars: K = TypeVar('K'); V = TypeVar('V') for dict-like classes: class Mapping(Generic[K, V]):.
  • Bounds: T = TypeVar('T', bound=str) restricts to subclasses of str.
  • Variance: TypeVar('T', contravariant=True) for input-only types.

Mypy in Practice

Save the Stack class to stack.py. Run mypy stack.py—no errors for valid code.

Test errors: Add stack: Stack[int] = Stack[str]() then mypy stack.py:

stack.py: error: Incompatible types in assignment (expression has type "Stack[str]", variable has type "Stack[int]")  [assignment]

Fix by matching types. Correct usage passes silently.

Practical Benefits and Tools

Generics catch errors early in IDEs and CI pipelines. Run mypy script.py to validate. No performance hit—type hints erase at runtime. Ideal for libraries like FastAPI or Pydantic.