Metaclasses — Production Patterns
A class is an instance of a metaclass (type by default). Metaclasses customize how classes themselves are created, before the first instance ever exists. Django ORM, SQLAlchemy, pydantic, abc — all use metaclasses to make their magic feel native.
The interview question: “When would you actually use one?” The honest answer: rarely; __init_subclass__ covers most cases. But knowing both is the senior signal.
The mental model
class Foo:
pass
# Equivalent to (conceptually):
Foo = type("Foo", (), {}) # name, bases, dict
type is a class. Its instances are other classes. When Python parses a class statement, it calls type(name, bases, namespace) to build the class object.
A metaclass is a class whose instances are classes. You define one by subclassing type:
class TracedMeta(type):
def __new__(mcs, name, bases, namespace):
print(f"Creating class {name}")
return super().__new__(mcs, name, bases, namespace)
class MyClass(metaclass=TracedMeta):
pass
# Prints: "Creating class MyClass"
__new__ on a metaclass intercepts class creation. __init__ on a metaclass intercepts post-creation customization.
What metaclasses can do
1. Register subclasses automatically (Django models)
class ModelMeta(type):
registry = {}
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if name != "Model": # don't register the base
ModelMeta.registry[name] = cls
return cls
class Model(metaclass=ModelMeta):
pass
class User(Model):
pass
class Order(Model):
pass
print(ModelMeta.registry)
# {'User': <class 'User'>, 'Order': <class 'Order'>}
Django uses this pattern: every class Foo(models.Model) gets registered with the ORM, gets a _meta attribute, gets a Manager, gets a default __init__ from declared fields. The user writes simple declarative code; the metaclass does the wiring.
2. Validate or enforce class structure
class InterfaceMeta(type):
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if bases: # not the base class itself
for attr in mcs.required:
if not hasattr(cls, attr):
raise TypeError(f"{name} missing required {attr}")
return cls
class Repository(metaclass=InterfaceMeta):
required = ["save", "find", "delete"]
Any class with Repository ancestor must define save, find, delete — checked at class definition, not at instance time. Loud failure at import.
3. Inject methods / attributes
class AddReprMeta(type):
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
cls.__repr__ = lambda self: f"<{name} at {id(self)}>"
return cls
Decorate-like behavior without an explicit decorator. Used historically by libraries pre-@dataclass.
4. Singleton
class SingletonMeta(type):
_instances = {}
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):
self.url = url
a = Database("postgres://...")
b = Database("postgres://...")
assert a is b
The metaclass’s __call__ intercepts Database(...) — the class itself acting as a callable — and routes through a cache. There are simpler ways (@functools.cache-style); this is the metaclass-y way.
__init_subclass__ — the modern alternative
Python 3.6+ added __init_subclass__, which covers 90% of metaclass use cases without needing a metaclass:
class Model:
registry = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Model.registry[cls.__name__] = cls
class User(Model):
pass
class Order(Model):
pass
print(Model.registry)
# {'User': <class 'User'>, 'Order': <class 'Order'>}
Same behavior, no metaclass needed. The base class hooks into “subclass was created” without changing the metaclass.
When __init_subclass__ is enough
- Registry pattern.
- Validate the subclass has required attrs.
- Inject methods into subclasses.
- Pass kwargs to subclasses via
class Foo(Base, kwarg=value):.
When you still need a metaclass
- You want to customize the namespace (the
classbody’s local scope).__init_subclass__runs after the namespace is built; metaclass__new__can mutate it. - You need to override
__call__on the class (controlling instance creation). - You need to integrate with multiple inheritance where the bases have different metaclasses — Python requires a common metaclass.
- You need to control behavior of the class itself (not just its subclasses).
Django still uses metaclasses for models.Model partly for historical reasons, partly for __call__ customization (the Model.__init__ accepts kwargs derived from fields declared in the namespace).
Pydantic v2 — the modern approach
Pydantic v2 uses metaclass + descriptor + custom __init__ generation:
class ModelMetaclass(ABCMeta):
def __new__(mcs, cls_name, bases, namespace, **kwargs):
cls = super().__new__(mcs, cls_name, bases, namespace, **kwargs)
cls.__pydantic_init__()
return cls
The metaclass calls a __pydantic_init__() method that processes annotations, builds the validation schema (via the Rust core), generates the JSON schema, etc.
In modern Python, libraries combine metaclasses + __init_subclass__ + descriptors + decorators. There’s no single “right” tool; each handles one piece.
Metaclass conflicts in multiple inheritance
class MetaA(type): pass
class MetaB(type): pass
class A(metaclass=MetaA): pass
class B(metaclass=MetaB): pass
class C(A, B): pass # TypeError: metaclass conflict
Python can’t decide which metaclass C should use. Fix: a common metaclass that inherits from both:
class MetaAB(MetaA, MetaB): pass
class C(A, B, metaclass=MetaAB): pass # works
This is the gotcha when combining libraries that each use a metaclass (e.g., Django + ABCMeta).
Performance
Metaclass overhead is at class creation, not instance creation. Each class statement runs the metaclass’s __new__ once. No cost per instance.
For libraries that generate many small classes dynamically, this can add up — pydantic optimizes class creation aggressively. For your own code where classes are defined once, the cost is invisible.
When to NOT use a metaclass
The classic Tim Peters quote: “If you wonder whether you need metaclasses, you don’t. (The people who actually need them know they need them, and don’t need an explanation about why.)”
Translate: metaclasses are powerful but rarely necessary. If a simpler tool works (__init_subclass__, decorator, base class with mixin), use that. Code that uses metaclasses is harder to read, harder to debug, harder for IDEs to introspect.
Decision matrix
| Need | Tool |
|---|---|
Generate __init__, __repr__, __eq__ from fields |
@dataclass, @attrs.define |
| Validate fields at runtime + JSON schema | Pydantic |
| Auto-register subclasses | __init_subclass__ |
| Validate subclass has required attrs | __init_subclass__ |
| Make class instances singletons | __call__ on metaclass, or factory function |
| Build a DSL (Django models, SQLAlchemy ORM) | metaclass (or combine with descriptors) |
| Add methods to every subclass | __init_subclass__ |
| Modify class namespace at creation | metaclass |
__init_subclass__ covers ~80%. Decorators cover another 10%. Metaclasses are for the last 10% — frameworks doing real magic.
abc.ABCMeta
The stdlib metaclass for abstract base classes:
from abc import ABC, abstractmethod
class Repository(ABC):
@abstractmethod
def save(self, obj): ...
Instantiating a class that hasn’t implemented all @abstractmethods raises TypeError at construction time. The mechanism is a metaclass (ABCMeta) that hooks into __call__.
In modern code, Protocol (structural typing) often replaces ABC — no inheritance required, just attribute / method matching.
Interview angle
- “What is a metaclass?” — a class whose instances are other classes.
typeis the default metaclass. Subclasstypeand usemetaclass=YourMetato customize class creation. Allows hooking into the construction of classes themselves, not just their instances. - “When would you actually use a metaclass?” — almost never in application code. Frameworks (Django ORM, SQLAlchemy, pydantic) use them to make declarative APIs feel native. For 90% of “I want to do something when a subclass is created” cases, use
__init_subclass__instead. - “What’s
__init_subclass__and how does it differ from a metaclass?” — a hook called when a subclass is created. Lets you register, validate, inject — without writing a metaclass. Cleaner, simpler, IDE-friendlier. Use it first. - “Django uses a metaclass for
models.Model. Why not just__init_subclass__?” — partly historical; Django predates__init_subclass__. Also: the metaclass customizes__call__(creating instances from declared fields) and processes the class namespace at definition.__init_subclass__happens after the namespace is built. - “What’s a metaclass conflict in multiple inheritance?” — Python can’t decide which metaclass a subclass should use when its bases have different metaclasses. Fix: define a common metaclass inheriting from both, use it explicitly. Common when combining Django models with ABCMeta.
- “Performance cost of metaclasses?” — at class creation time only, not per-instance. For application code where classes are defined once at import, the cost is invisible. Frameworks generating many classes dynamically (per-request, per-tenant) may notice.