32.7 __class_getitem__ and Generic Classes
In Python, the __class_getitem__ special method is a cornerstone for creating generic classes—classes that are parameterized by one or more types. Introduced in Python 3.7 via PEP 560, it provides a mechanism for classes to support square-bracket notation ([]) for type hinting purposes without immediately creating a new class or metaclass. This method is fundamentally different from __getitem__ which operates on instances; __class_getitem__ is called on the class itself.
The Purpose of class_getitem
The primary purpose of __class_getitem__ is to enable type parameterization for static type checkers. When you write list[int], you are not, in fact, creating a new type of list at runtime. For the Python interpreter, this operation is largely ornamental. Its real consumer is a static type checker like mypy or Pyright. These tools understand that list[int] signifies a list where all elements are of type int. The __class_getitem__ method allows a class to define what should be returned when this subscripting syntax is used on it, enabling the rich ecosystem of generic type hints we have today.
Basic Implementation and Behavior
A minimal implementation of __class_getitem__ simply returns the class itself, optionally modified. This is the standard pattern for most generic classes, as the runtime behavior of the class should not change; only its type annotation does.
from typing import Generic, TypeVar
T = TypeVar('T')
class Box(Generic[T]):
def __init__(self, item: T):
self.item = item
@classmethod
def __class_getitem__(cls, key):
# For a simple generic class, we can just return the class itself.
# The 'Generic[T]' base class provides a more sophisticated version of this.
return cls
# Usage at runtime: This doesn't create a new class.
box_of_int = Box[int] # This calls Box.__class_getitem__(int)
print(box_of_int is Box) # Output: True
# The type parameter is meaningful for type checkers, not the runtime.
my_box = Box(42) # Type checker sees this as Box[int]
How It Integrates with typing.Generic
The typing.Generic base class provides the canonical implementation of __class_getitem__. When you inherit from Generic[T], you inherit a __class_getitem__ method that handles the creation and caching of parameterized generic aliases. This is why you rarely need to implement __class_getitem__ yourself.
from typing import Generic, TypeVar
T_co = TypeVar('T_co', covariant=True)
class ReadOnlyBox(Generic[T_co]):
def __init__(self, item: T_co):
self._item = item
def get(self) -> T_co:
return self._item
# Generic.__class_getitem__ creates a special object.
alias = ReadOnlyBox[float]
print(type(alias)) # Output: <class 'typing._GenericAlias'>
print(alias) # Output: __main__.ReadOnlyBox[float]
# This alias can be used to create instances.
float_box = alias(3.14)
value = float_box.get() # Runtime type is still just 'ReadOnlyBox'
Common Pitfalls and Misconceptions
A significant pitfall is the assumption that SomeClass[Param] creates a distinct new class at runtime. It does not. It typically creates an instance of types.GenericAlias or a similar object. This means that class-level attributes, methods, or metaclasses are shared across all parameterizations.
class MisguidedGeneric:
type_param = None
@classmethod
def __class_getitem__(cls, param):
cls.type_param = param # This mutates the class itself!
return cls
MisguidedGeneric[str]
print(MisguidedGeneric.type_param) # Output: <class 'str'>
MisguidedGeneric[int]
print(MisguidedGeneric.type_param) # Output: <class 'int'> (the str is gone!)
Another pitfall involves inheritance. If a base class defines __class_getitem__ and a subclass does not, the subclass will inherit the base class’s method, which may return the base class instead of the subclass when subscripted.
Advanced Usage: Custom Parameterized Types
While typing.Generic suffices for most use cases, you might need to implement a custom __class_getitem__ for advanced scenarios, such as creating a specialized alias or validating type parameters at declaration time.
def validate_type(param):
if not isinstance(param, type):
raise TypeError("Class subscript must be a type.")
class StrictBox:
def __init__(self, item):
self.item = item
@classmethod
def __class_getitem__(cls, param):
validate_type(param)
# Create a _GenericAlias-like object for consistency with the typing module.
from types import GenericAlias
return GenericAlias(cls, param)
# This will work and be understood by type checkers.
IntBox = StrictBox[int]
# This will raise a TypeError at the point of class subscripting.
try:
BadBox = StrictBox[42]
except TypeError as e:
print(e) # Output: Class subscript must be a type.
Best Practices
- Prefer
typing.Generic: Unless you have a very specific reason, always inherit fromtyping.Genericto make your class generic. Its implementation is robust, cached, and understood by all type checkers. - Idempotence is Key: Your
__class_getitem__implementation should be idempotent. CallingYourClass[Param]multiple times should return the same or equivalent objects. Thetypingmodule caches these aliases for performance. - Don’t Mutate State: The method should not mutate the class or any global state. Its purpose is to return an object representing the parameterized type, not to have side effects.
- Return Appropriate Objects: For consistency with the standard library, consider returning a
types.GenericAliasobject when implementing a custom method. This ensures better interoperability with other Python features and typing tools.