Introduction to Generics
- Generic types let you write reusable, type-safe functions and classes that work uniformly across different data types.
- They preserve the relationship between input and output types, enabling MyPy to infer precise types instead of falling back to
Any.
- The
typing module’s TypeVar and Generic primitives unlock this capability.
The Need for Generics
- Annotating with
Any sacrifices type information, so tools cannot guarantee correct usage of returned values.
- A generic abstraction retains knowledge of the specific type in each context, improving IDE support and static checks.
- For example, a "first-item" function should return
str for a list[str] and int for a list[int], not just Any.
Defining Type Variables
T = TypeVar('T') declares a placeholder type variable T that can stand for any type.
- A function annotated
def get_first_item_generic(data: List[T]) -> Optional[T]: returns an element of the same type as the list elements.
- MyPy infers
T from each call site, preserving specific return types like Optional[str] or Optional[int].
Constrained Type Variables
- When a generic should only accept certain types, constrain it:
NumberType = TypeVar('NumberType', int, float).
- Functions like
def add_generic_numbers(x: NumberType, y: NumberType) -> NumberType: then only accept int or float and return that same type.
- Constrained type variables combine flexibility with necessary restrictions for safe operations.
Bounded Type Variables
- When a generic should only accept subclasses of a specific superclass, we can use a type bound, constrain it:
NumberType = TypeVar('NumberType', bound=Superclass).
- Functions like
def add_generic_numbers(x: NumberType, y: NumberType) -> NumberType: then accept any subclass of Superclass, and they can be different subclasses for each argument.
- Like constrained type variables, bounded type variables provide useful functionalities combining flexibility and type safety.
Generic Classes
- Inherit from
Generic[T] to define a class parameterized by a type variable T.
- A class like
SimpleStack[T] can push, pop, and peek items of type T, and MyPy will enforce that only T instances are used.
- This pattern creates custom container types that maintain strong type guarantees for their contents.
Common Pitfalls & How to Avoid Them
- A class that uses
T in its methods but does not inherit from Generic[T] is not recognized as generic by MyPy.
- Unconstrained
TypeVar('T') can degrade type safety when operations require certain capabilities—use bounds or explicit type lists when appropriate.
from typing import Optional, TypeVar, Generic
# Section: Defining a generic function to get the first item of a list
T = TypeVar("T")
def get_first_item(
input_list: list[T],
) -> Optional[T]:
if input_list:
return input_list[0]
return None
first_number = get_first_item([1, 2, 3])
first_str = get_first_item(["abc", "def"])
first_mixed_list = get_first_item(["abc", "def", 1, 2, 3])
# Section: Constrained TypeVar for numeric addition
NumberType = TypeVar("NumberType", int, float)
def add_generic_numbers(
x: NumberType, y: NumberType
) -> NumberType:
return x + y
sum_int = add_generic_numbers(3, 5.0)
# Section: Bounded TypeVar with deployed filter for DevOps resources
class CloudResource:
def __init__(self, name: str, cpu_usage: float) -> None:
self.name = name
self.cpu_usage = cpu_usage
self.deployed: bool = False
def deploy(self) -> None:
print(f"Deploying {self.name}")
self.deployed = True
class VirtualMachine(CloudResource):
def reboot(self) -> None:
print(f"Rebooting VM {self.name}")
class DockerContainer(CloudResource):
def restart(self) -> None:
print(f"Restarting container {self.name}")
ResourceType = TypeVar("ResourceType", bound=CloudResource)
def filter_deployed(
resources: list[ResourceType],
) -> list[ResourceType]:
return [
resource for resource in resources if resource.deployed
]
vm1 = VirtualMachine("vm-01", cpu_usage=65.0)
vm2 = VirtualMachine("vm-02", cpu_usage=45.0)
container1 = DockerContainer("api-service", cpu_usage=85.0)
container2 = DockerContainer("worker", cpu_usage=55.0)
vm1.deploy()
container1.deploy()
all_resources = [vm1, vm2, container1, container2]
deployed_resources = filter_deployed(all_resources)
# Section: Generic class SimpleStack
G = TypeVar("G")
class SimpleStack(Generic[G]):
def __init__(self) -> None:
self._items: list[G] = []
def push(self, item: G) -> None:
self._items.append(item)
def pop(self) -> G:
if self.is_empty():
raise IndexError("Stack is empty!")
return self._items.pop()
def peek(self) -> Optional[G]:
if self.is_empty():
return None
return self._items[-1]
def is_empty(self) -> bool:
return not self._items
str_stack = SimpleStack[str](http://)
str_stack.push("str")
int_stack = SimpleStack[int](http://)
int_stack.push(12)