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

  1. Prefer typing.Generic: Unless you have a very specific reason, always inherit from typing.Generic to make your class generic. Its implementation is robust, cached, and understood by all type checkers.
  2. Idempotence is Key: Your __class_getitem__ implementation should be idempotent. Calling YourClass[Param] multiple times should return the same or equivalent objects. The typing module caches these aliases for performance.
  3. 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.
  4. Return Appropriate Objects: For consistency with the standard library, consider returning a types.GenericAlias object when implementing a custom method. This ensures better interoperability with other Python features and typing tools.