Metaclasses and Class Creation
What happens when a class statement executes, why __init_subclass__ and decorators solve most of what people reach for metaclasses to do, and the narrow cases that genuinely need one.
What happens when a class statement executes, why __init_subclass__ and decorators solve most of what people reach for metaclasses to do, and the narrow cases that genuinely need one.
Every plugin in your system must register itself, and every one of them does it with the same three lines copied at the bottom of the file. Someone adds a plugin, forgets the lines, and it silently never runs. You want the registration to happen because the class exists, not because someone remembered.
That is a real use for customising class creation, and
metaclasses are the tool people reach for. They are also the
tool people reach for when two simpler things would have worked
better. By the end of this lesson you will know what happens
when a class statement runs, the three ways to hook into it,
and why the metaclass is the last of the three you should try.
class Photo:
kind = "image"
def caption(self) -> str:
return self.nameWhen Python reaches that, it does four things:
Execute the body
As ordinary code, in a fresh namespace. Assignments, def
statements, even a print — all of it runs now, and the
resulting names end up in a dictionary.
Determine the metaclass
Normally type.
Call it
With three arguments: the name, the bases, and that namespace dictionary.
Bind the result
To the name Photo.
You can do it by hand:
def caption(self) -> str:
return self.name
Photo = type("Photo", (), {"kind": "image", "caption": caption})That is genuinely equivalent. type is not only the function
that reports an object's type — called with three arguments it
creates a class, because a class is an object and type is
the class that classes are instances of.
print(type(42)) # <class 'int'>
print(type(int)) # <class 'type'>
print(type(type)) # <class 'type'>A metaclass is whatever creates the class, the way a class creates an instance.
For the registry problem, no metaclass is needed:
class Plugin:
registry: dict[str, type["Plugin"]] = {}
def __init_subclass__(cls, /, name: str | None = None, **kwargs):
super().__init_subclass__(**kwargs)
key = name or cls.__name__.lower()
if key in Plugin.registry:
raise ValueError(f"duplicate plugin name {key!r}")
Plugin.registry[key] = clsclass ResizePlugin(Plugin, name="resize"):
...
class WatermarkPlugin(Plugin):
...
print(Plugin.registry) # {'resize': ..., 'watermarkplugin': ...}__init_subclass__ is called on the parent whenever a subclass
is defined. It receives the new class, it can take keyword
arguments from the class statement, and it can validate,
register, or reject.
This is the tool that removed most legitimate metaclass use. It is an ordinary method on an ordinary class, it composes with inheritance, and a reader who has never heard of it can still guess what it does.
The implicit classmethod is worth noting: __init_subclass__
is one automatically, so you do not write the decorator.
The other simpler tool takes a finished class and returns one:
def register(name: str):
def decorator(cls: type) -> type:
Plugin.registry[name] = cls
return cls
return decorator
@register("resize")
class ResizePlugin:
...Decorators run after the class exists, which makes them ideal
for inspecting or adding to it — @dataclass is exactly this,
reading the annotations and adding __init__, __repr__ and
__eq__.
The trade against __init_subclass__: a decorator does not
require inheritance, so it works on classes you do not control
and does not force a base class into your hierarchy. But it is
opt-in — someone can forget it — which is precisely what
__init_subclass__ prevents.
Choose on that axis. Must every subclass do this?
__init_subclass__. Should this particular class do this?
A decorator.
class ValidatedMeta(type):
def __new__(mcls, name, bases, namespace, **kwargs):
if bases: # skip the base itself
if "process" not in namespace:
raise TypeError(
f"{name} must define process()"
)
return super().__new__(mcls, name, bases, namespace, **kwargs)
class Plugin(metaclass=ValidatedMeta):
...
class BadPlugin(Plugin):
pass
# TypeError: BadPlugin must define process()The failure happens at import time, when the class is defined, rather than when someone calls the missing method.
What a metaclass can do that the other two cannot is narrow:
Act before the class exists. __new__ receives the raw
namespace and can change it — adding, removing or rewriting
members before the class object is built.
Control the namespace itself. __prepare__ returns the
mapping the class body executes in, so you can use an ordered or
recording dictionary and know the definition order or catch
duplicate names.
Customise the class's own behaviour. Methods on the
metaclass are methods of the class, so __call__ on it
intercepts instance creation, and __getattr__ on it handles
missing class attributes.
Everything else — validating, registering, adding methods, wrapping — is available from the two simpler tools.
Bad — a metaclass to add a computed attribute.
class TableMeta(type):
def __new__(mcls, name, bases, namespace, **kwargs):
namespace["table_name"] = name.lower() + "s"
return super().__new__(mcls, name, bases, namespace, **kwargs)
class Photo(metaclass=TableMeta):
...
print(Photo.table_name) # photosGood — the same thing, in a place people will find it.
class Model:
table_name: str
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.table_name = cls.__name__.lower() + "s"
class Photo(Model):
...Both work. The first puts the behaviour in a class that never
appears where Photo is defined, so a reader wondering where
table_name comes from finds no assignment, no property and no
inheritance to follow — they have to know that
metaclass=TableMeta implies executable code somewhere else.
A type checker does not follow it either, so Photo.table_name
is an error in a checked codebase.
The second is a method on a base class, which shows up in the
MRO, in help(), in an editor's go-to-definition, and in the
annotation. It costs nothing and is findable.
Reading metaclasses matters more than writing them, and there are three you will meet.
abc.ABCMeta, behind ABC, which makes abstract methods
prevent instantiation:
from abc import ABC, abstractmethod
class Plugin(ABC):
@abstractmethod
def process(self, photo: Photo) -> Photo: ...That is the validating metaclass from earlier, already written, standard, and understood by every tool. Reach for it before writing your own.
enum.EnumMeta, which turns class-body assignments into
singleton members and makes the class iterable.
ORM and serialisation model bases — Django models,
SQLAlchemy's declarative base, older pydantic — which collect
descriptor-like field definitions from the namespace and build a
schema. This is the genuine case: they must inspect the raw
namespace, and they own the whole hierarchy.
WHAT HAPPENS
a class body EXECUTES into a namespace dict
the metaclass is called: type(name, bases, namespace)
Photo = type("Photo", (), {...}) is the same thing
a metaclass is to a class what a class is to an instance
THE THREE HOOKS, IN ORDER OF PREFERENCE
1 __init_subclass__(cls, **kwargs)
on the PARENT, runs for every subclass
register, validate, set derived attributes
implicitly a classmethod; forwards **kwargs from
`class X(Base, name="...")`
MUST every subclass do this? -> this one
2 a class decorator
runs AFTER the class exists; no inheritance required
what @dataclass is
SHOULD this class do this? -> this one
but it can be forgotten
3 a metaclass
__new__ sees and can rewrite the namespace BEFORE creation
__prepare__ controls the mapping the body executes in
__call__ on it intercepts instance creation
only when you need one of those three
COSTS
one metaclass per class, and it must subclass those of all
bases -> "metaclass conflict" for anyone who inherits
invisible at the definition site
type checkers do not follow it
MEET THEM HERE
abc.ABCMeta abstract methods; use ABC, do not rewrite it
enum.EnumMeta members and iteration
ORM model bases the legitimate case: read the raw namespaceYou now know what a class statement actually does, and that
almost everything people write metaclasses for is better served
by __init_subclass__ or a decorator. The judgement to keep is
about cost: a metaclass is invisible where the class is defined
and constrains everyone who inherits, so it needs to buy
something the simpler hooks cannot.
Next is Attribute Lookup and the MRO, which finishes the
object model. You have seen where attributes come from on one
class; that lesson covers what happens with several, why
super() is not "call the parent", and what __slots__
changes.
Before you move on, take a base class you have and add an
__init_subclass__ that rejects subclasses missing a required
method. Then define one that violates it and watch the error
arrive at import rather than at call time. Moving a failure from
runtime to import is most of what this lesson is for.