Python Generic Class: Typing Reusable Components
python generic class: Learn how to declare and use generic classes in Python with TypeVar and typing.Generic to build type-safe, reusable components.
When you write a class that should work with multiple types while preserving type information, a python generic class lets you declare that relationship explicitly. The typing module provides TypeVar and Generic to define classes that accept type parameters, so static type checkers can verify usage without forcing you to duplicate code.
Declaring a Generic Class with TypeVar
The core of a generic class is a TypeVar, which acts as a placeholder for a type that will be supplied later. To create a generic class, you define a TypeVar, then inherit from Generic[T].
from typing import TypeVar, Generic T = TypeVar("T") class Box(Generic[T]): def __init__(self, item: T) -> None: self._item = item def get(self) -> T: return self._item
Here, T is a type variable that can be bound to any concrete type when the class is instantiated. For example, Box[int] and Box[str] are distinct types, and a type checker will enforce that get() returns the same type that was passed to the constructor. This is the primary benefit: you get compile-time type checking without writing separate implementations for each type.
Using Multiple Type Parameters
A generic class can accept more than one type parameter. This is useful for containers that pair two related types, such as a dictionary-like wrapper or a repository that maps keys to values.
from typing import TypeVar, Generic K = TypeVar("K") V = TypeVar("V") class KeyValueStore(Generic[K, V]): def __init__(self) -> None: self._data: dict[K, V] = {} def set(self, key: K, value: V) -> None: self._data[key] = value def get(self, key: K) -> V | None: return self._data.get(key)
When you use KeyValueStore[str, int], the type checker knows that set expects a string key and an integer value, and get returns int | None. Multiple parameters allow you to model relationships precisely, which reduces the chance of accidentally mixing incompatible types.
Constraining Type Parameters with Upper Bounds
Sometimes you want a generic class to accept only types that share a common base. You can set an upper bound on a TypeVar so that the type parameter must be a subtype of that bound.
from typing import TypeVar, Generic from collections.abc import Iterable T = TypeVar("T", bound=Iterable) class Repeater(Generic[T]): def __init__(self, iterable: T) -> None: self._iterable = iterable def repeat(self, times: int) -> list[object]: return list(self._iterable) * times
Here, T must be an Iterable. You can still use Repeater[list[int]] or Repeater[str], but not Repeater[int] because an integer is not iterable. Bounds are useful when the class relies on methods or attributes of the bound. They also make the intent explicit: the class only works with types that provide a certain interface.
Variance and How It Affects Subtyping
Variance determines how generic types relate when their type parameters are subclasses of each other. Python's typing module lets you specify variance on a TypeVar using covariant=True or contravariant=True. By default, type variables are invariant, meaning Box[Cat] is not a subtype of Box[Animal] even if Cat is a subtype of Animal.
from typing import TypeVar, Generic T_co = TypeVar("T_co", covariant=True) class Producer(Generic[T_co]): def __init__(self, value: T_co) -> None: self._value = value def get(self) -> T_co: return self._value
Covariance allows Producer[Cat] to be treated as a subtype of Producer[Animal] because the class only produces values of type T_co. Contravariance, on the other hand, applies to consumers that only accept values. Understanding variance is important when designing generic classes that are meant to be used polymorphically. Most classes that only read from a type parameter should be covariant; those that only write to it should be contravariant. If a class both reads and writes, it must remain invariant.
Runtime Behavior: What Generics Actually Do
Generics in Python are primarily a static typing feature. At runtime, Box[int] and Box[str] are the same class object. The type parameters are not stored, and they do not affect how the class behaves. This means you cannot use a type parameter to make runtime decisions, such as checking the type of an attribute or performing different operations based on the type argument.
box = Box[int](42) print(type(box)) # <class '__main__.Box'>
There is no Box[int] class created at runtime. The [] syntax is just a hint for type checkers. This has practical implications: if you need runtime type validation, you must implement it separately, for example by inspecting __orig_class__ or using isinstance checks. Generics do not replace runtime validation; they only help you catch type errors before the code runs.
Maintainability: When Generics Pay Off
Generic classes shine in codeb that reuse the same logic across multiple types. Without generics, you either duplicate the class for each type or use Any, which disables type checking. Both approaches increase maintenance burden. A generic class keeps the implementation in one place while preserving type safety for every instantiation.
Consider a repository pattern that works with different database models. A generic Repository[T] can define common operations like get, save, and delete without knowing the concrete model. Each model gets its own typed repository instance, and type errors surface at development time rather than during a database call.
The tradeoff is added complexity. For a small script or a class used only once, introducing a TypeVar and Generic may be overkill. The decision should be based on how many distinct types the class will serve and how much type safety you need. If you are building a public API or a library that other developers will use, generics are often worth the extra syntax.
Common Mistakes and How to Avoid Them
One frequent mistake is using TypeVar without inheriting from Generic[T]. The TypeVar alone does not make a class generic; you must explicitly inherit from Generic[T] for the type checker to treat it as such.
Another mistake is using a concrete type in the Generic base class, like Generic[int]. This is not an error, but it makes the class non-generic and defeats the purpose. The type parameter should be a TypeVar, not a fixed type.
A third issue is ignoring variance when designing class hierarchies. If you mark a TypeVar as covariant but the class also accepts values of that type through a setter, the type checker will raise an error because the class is no longer purely a producer. The solution is to either remove the setter or keep the TypeVar invariant.
Finally, do not expect generics to provide runtime safety. They are a development-time tool. If you need to validate types at runtime, use isinstance or a validation library. Generics and runtime validation solve different problems, and combining them appropriately leads to more robust code.