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 ofstr. - 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.
Leave a Reply