Skip to content

Metaclasses#

A metaclass is the class of a class. In Python, type is the default metaclass for every new-style class. Metaclasses let you intercept class creation — registering subclasses, enforcing APIs, injecting methods, or building DSLs — before instances ever exist.

How to use this page

Read after Classes and OOP Basics and Dunder or Magic Methods. Pair with type() for Dynamic Class Creation and Class Decorators — they solve overlapping problems with different ergonomics.

At a glance
Track Python Advanced → Meta Programming
Sections 11 major topics
Outline Use the right-hand TOC to jump

Topics: The object model in one pass · How class statements really work · Defining a custom metaclass · Practical pattern: enforcing required methods · Practical pattern: automatic registration · Metaclass vs class decorator · Metaclass inheritance and the metaclass= keyword · __prepare__ — controlling the class namespace (Python 3.6+) · … (+3 more)

  1. The object model in one pass
  2. How class statements really work
  3. Defining a custom metaclass
  4. Practical pattern: enforcing required methods
  5. Practical pattern: automatic registration
  6. Metaclass vs class decorator
  7. Metaclass inheritance and the metaclass= keyword
  8. __prepare__ — controlling the class namespace (Python 3.6+)
  9. Singleton via metaclass __call__
  10. Debugging metaclass code
  11. When not to use metaclasses

The object model in one pass#

class Dog:
    species = "Canis"

d = Dog()
Expression What it evaluates to
type(d) Dog (the class)
type(Dog) type (the metaclass)
type(type) type (metaclass of itself)
isinstance(d, Dog) True
isinstance(Dog, type) True

Mental model: instances are made by classes; classes are made by metaclasses. The chain stops at type because type is its own metaclass.

>>> Dog.__class__
<class 'type'>
>>> d.__class__
<class '__main__.Dog'>

How class statements really work#

Writing class Foo: pass is syntactic sugar. Python roughly does:

namespace = {}
# body of class Foo executes here, populating namespace
Foo = type("Foo", (object,), namespace)

Three arguments to type(name, bases, dict):

Argument Role
name Class name string
bases Tuple of base classes
dict Namespace: methods, attributes, __module__, etc.

When you declare metaclass=M, Python calls M(name, bases, dict) instead of type(...). Details: type() for Dynamic Class Creation.


Defining a custom metaclass#

A metaclass is any callable that accepts (name, bases, namespace) and returns a class. Conventionally it subclasses type:

class Meta(type):
    def __new__(mcs, name, bases, namespace, **kwargs):
        # mutate namespace before class object exists
        namespace["created_by"] = "Meta"
        return super().__new__(mcs, name, bases, namespace)

    def __init__(cls, name, bases, namespace, **kwargs):
        super().__init__(name, bases, namespace)
        cls.registry: list[type] = []

    def __call__(cls, *args, **kwargs):
        # runs when you call Dog() — instance creation
        print(f"Creating instance of {cls.__name__}")
        return super().__call__(*args, **kwargs)


class Dog(metaclass=Meta):
    def bark(self) -> str:
        return "woof"


d = Dog()
# Creating instance of Dog

__new__ vs __init__ on the metaclass#

Method When it runs Typical use
Meta.__new__ Before the class object exists Filter/rename attributes, reject invalid classes
Meta.__init__ After the class object exists Register class, attach class-level metadata
Meta.__call__ When Cls() is invoked Singleton enforcement, logging, pooling

Prefer __new__ for namespace surgery

__init__ on the metaclass cannot return a replacement class; __new__ can. Most validation and namespace mutation belongs in __new__.


Practical pattern: enforcing required methods#

class EnforceMethods(type):
    def __new__(mcs, name, bases, namespace, required=(), **kwargs):
        cls = super().__new__(mcs, name, bases, namespace)
        if bases:  # skip the abstract base itself
            missing = [m for m in required if m not in namespace]
            if missing:
                raise TypeError(
                    f"{name} must define: {', '.join(missing)}"
                )
        return cls


class Serializer(metaclass=EnforceMethods, required=("serialize",)):
    pass


class JSONSerializer(Serializer):
    def serialize(self, obj) -> str:
        import json
        return json.dumps(obj)


# class Broken(Serializer): pass
# TypeError: Broken must define: serialize

For production APIs, prefer abc.ABC — clearer errors, better tooling. Metaclasses shine when you need cross-cutting behavior on every subclass without inheritance boilerplate.


Practical pattern: automatic registration#

class PluginMeta(type):
    registry: dict[str, type] = {}

    def __new__(mcs, name, bases, namespace, **kwargs):
        cls = super().__new__(mcs, name, bases, namespace)
        if bases:  # concrete plugin
            PluginMeta.registry[name.lower()] = cls
        return cls


class Plugin(metaclass=PluginMeta):
    pass


class CsvPlugin(Plugin):
    def run(self) -> None:
        print("csv")


class JsonPlugin(Plugin):
    def run(self) -> None:
        print("json")


PluginMeta.registry
# {'csvplugin': <class 'CsvPlugin'>, 'jsonplugin': <class 'JsonPlugin'>}

Frameworks like Django ORM and SQLAlchemy use metaclass machinery (or equivalent hooks) to collect model definitions at import time.


Metaclass vs class decorator#

Approach Runs when Best for
Metaclass Class body fully executed, before class object finalized Subclass enforcement, registries, altering __new__ on instances
Class decorator After class object already exists Adding wrappers, simple mixins, post-hoc registration
def add_repr(cls):
    def __repr__(self):
        attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
        return f"{cls.__name__}({attrs})"
    cls.__repr__ = __repr__
    return cls


@add_repr
class Point:
    def __init__(self, x: int, y: int):
        self.x, self.y = x, y

See Class Decorators. Rule of thumb: if you need to control how the class is constructed, reach for a metaclass; if you need to tweak an existing class, use a decorator.


Metaclass inheritance and the metaclass= keyword#

class MetaA(type):
    pass

class MetaB(MetaA):
    pass

class Base(metaclass=MetaA):
    pass

class Child(Base, metaclass=MetaB):
    pass

type(Child)  # MetaB — explicit metaclass wins if compatible

Python 3 requires metaclass compatibility along the MRO. Conflicting metaclasses raise TypeError at class definition time — not at instantiation.

Interview trap — metaclass on a subclass

You cannot casually add a different metaclass to a subclass of a metaclass-controlled base. The metaclass of the most derived class must be a subclass of the metaclasses of all bases.


__prepare__ — controlling the class namespace (Python 3.6+)#

class OrderedMeta(type):
    @classmethod
    def __prepare__(mcs, name, bases, **kwargs):
        from collections import OrderedDict
        return OrderedDict()


class Model(metaclass=OrderedMeta):
    z = 1
    a = 2
    m = 3

list(Model.__dict__.keys())  # declaration order preserved among own attrs

Useful for frameworks that need source-order field declarations (ORM columns, dataclass-like DSLs).


Singleton via metaclass __call__#

class SingletonMeta(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=SingletonMeta):
    def __init__(self, url: str):
        self.url = url


db1 = Database("postgres://localhost")
db2 = Database("mysql://ignored")
assert db1 is db2

Simpler alternatives exist

Module-level singletons, @functools.lru_cache on a factory, or dependency injection containers often beat metaclass singletons in readability.


Debugging metaclass code#

import inspect

class VerboseMeta(type):
    def __new__(mcs, name, bases, namespace, **kwargs):
        print(f"Creating {name} with bases {bases}")
        print(f"  methods: {[k for k, v in namespace.items() if callable(v)]}")
        return super().__new__(mcs, name, bases, namespace)

Pair with Inspect Module for Introspection to inspect resulting classes at runtime.


When not to use metaclasses#

Situation Better tool
Enforce interface abc.ABC + @abstractmethod
Add one method post-hoc Class decorator
Simple data record @dataclass
Runtime behavior change Monkey Patching (tests only)
Dynamic class from JSON schema type() or dataclasses.make_dataclass

The Zen applies

Metaclasses are powerful and non-local — readers must hunt for metaclass= to understand a class. Reserve them for framework code where the payoff justifies the cognitive cost.


Interview traps (quick reference)#

Trap What goes wrong Safe approach
"Metaclass and class decorator are identical" Miss that decorator runs after class exists Choose based on creation vs post-processing
Conflicting metaclasses in MI TypeError at class body Design single metaclass hierarchy
Mutating namespace after __new__ Changes don't appear on class Mutate namespace inside __new__ before super().__new__
__call__ on metaclass vs __new__ on class Wrong hook for instance creation Metaclass __call__Cls.__new__Cls.__init__
Overusing for validation Hard-to-read framework in app code abc, dataclass, or plain inheritance

Mental model checklist#

  1. What is type(Cls) for a class defined with class Cls: pass?
  2. At what point does a metaclass's __new__ run relative to the class body?
  3. How does Meta.__call__ relate to Cls()?
  4. When would you pick a metaclass over a class decorator?
  5. Why do Django-style ORMs use metaclasses (or equivalent) instead of decorators alone?

What's next#

Topic Page
Dynamic type() type() for Dynamic Class Creation
Class decorators Class Decorators
ABCs Abstract Base Classes
Dunder methods Dunder or Magic Methods
Introspection Inspect Module for Introspection