Skip to main content

What are metaclasses?

A metaclass is a class that describes the construction and behavior of other classes, similarly to how classes describe the construction and behavior of objects. The default metaclass is type, but it’s possible to use other metaclasses. Metaclasses allow you to create “a different kind of class”, such as Enums, NamedTuples, and singletons.
Mypy has special understanding of ABCMeta and EnumMeta.

Defining a metaclass

class M(type):
    pass

class A(metaclass=M):
    pass

Metaclass usage example

Mypy supports the lookup of attributes in the metaclass:
from typing import ClassVar, TypeVar

S = TypeVar("S")

class M(type):
    count: ClassVar[int] = 0
    
    def make(cls: type[S]) -> S:
        M.count += 1
        return cls()

class A(metaclass=M):
    pass

a: A = A.make()  # make() is looked up at M; result is object of type A
print(A.count)

class B(A):
    pass

b: B = B.make()  # Metaclasses are inherited
print(B.count + " objects created")  # Error: Unsupported operand types
The make() method is defined on the metaclass M and can be called on class A itself (not on instances of A).

Metaclass conflicts

Metaclasses pose requirements on the inheritance structure:
class M1(type): pass
class M2(type): pass

class A1(metaclass=M1): pass
class A2(metaclass=M2): pass

class B1(A1, metaclass=M2): pass  # Error: metaclass conflict
class B12(A1, A2): pass  # Error: metaclass conflict
At runtime, the above conflicting definitions raise:
TypeError: metaclass conflict: the metaclass of a derived class
must be a (non-strict) subclass of the metaclasses of all its bases

Limitations

Mypy has several limitations when working with metaclasses:
Mypy does not understand dynamically-computed metaclasses:
class A(metaclass=f()): ...  # Not supported
Mypy does not and cannot understand arbitrary metaclass code. Only simple, straightforward metaclasses are supported.
Mypy only recognizes subclasses of type as potential metaclasses.
Self is not allowed as an annotation in metaclasses as per PEP 673.

Working around builtin type issues

For some builtin types, mypy may think their metaclass is ABCMeta even if it’s type at runtime:
import abc

assert type(tuple) is type  # True at runtime

class M0(type): pass
class A0(tuple, metaclass=M0): pass  # Mypy Error: metaclass conflict

Common metaclass patterns

Singleton pattern

class Singleton(type):
    _instances: dict[type, object] = {}
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class Database(metaclass=Singleton):
    def __init__(self) -> None:
        self.connected = False

db1 = Database()
db2 = Database()
assert db1 is db2  # True

Registry pattern

from typing import ClassVar

class RegistryMeta(type):
    registry: ClassVar[dict[str, type]] = {}
    
    def __new__(mcs, name, bases, attrs):
        cls = super().__new__(mcs, name, bases, attrs)
        if name != 'Plugin':  # Don't register the base class
            RegistryMeta.registry[name] = cls
        return cls

class Plugin(metaclass=RegistryMeta):
    pass

class JSONPlugin(Plugin):
    pass

class XMLPlugin(Plugin):
    pass

print(RegistryMeta.registry)
# {'JSONPlugin': <class 'JSONPlugin'>, 'XMLPlugin': <class 'XMLPlugin'>}

Best practices

It’s better not to combine metaclasses and complex class hierarchies. Keep inheritance simple when using metaclasses.
If you need both abstract base classes and custom metaclass behavior, inherit from ABCMeta instead of type.
Metaclasses can be confusing. Add clear documentation explaining what your metaclass does and why it’s needed.
Before using metaclasses, consider if class decorators or __init_subclass__ would work better for your use case.

Build docs developers (and LLMs) love