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)
- The object model in one pass
- How
classstatements 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+)- Singleton via metaclass
__call__ - Debugging metaclass code
- When not to use metaclasses
The object model in one pass#
| 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.
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#
- What is
type(Cls)for a class defined withclass Cls: pass? - At what point does a metaclass's
__new__run relative to the class body? - How does
Meta.__call__relate toCls()? - When would you pick a metaclass over a class decorator?
- 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 |