Metaclasses
A metaclass is the class of a class - the object that creates classes and controls how they are created. You met the premise in the classes chapter (type(Book) is type); this chapter makes it a tool. A metaclass intercepts class creation - the moment class Foo(Base): executes - and can inspect, modify, or reject the class being built before it exists. The uses are real but narrow: enforcing interface contracts at definition time, auto-registering subclasses, generating methods from declarations, and building framework "models" (the ORM base class is the canonical example). The rule that keeps them sane: a metaclass is a framework mechanism, not a default design choice - most of the jobs people reach for it do are better done with a base class, a decorator, or __init_subclass__.
Classes Are Made by type
class Book:
title = "X"
# defining a class is equivalent to calling type with three args:
# type(name, bases, namespace)
Book2 = type("Book2", (), {"title": "Y", "__module__": __name__})
print(Book2.title) # 'Y' - a class, built without the class keyword
print(type(Book2)) # <class 'type'> - made by the default metaclass
print(issubclass(Book, object)) # True - everything descends from object,
# and every class is an instance of type (or a metaclass)
The class statement is syntactic sugar for a type(name, bases, namespace) call: the body executes into a namespace dict (the methods, the class attributes), and type combines the name, the bases, and that namespace into a new class object. The first argument to type in that call is the metaclass - by default type itself, or a base's metaclass if one is specified. So "what makes a class" is itself a callable you can replace, and replacing it is what a metaclass is.
Defining a Metaclass
class RegistryMeta(type):
"""Records every class created with this metaclass."""
registry: dict[str, type] = {}
def __new__(mcls, name, bases, namespace, **kwargs):
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
if name != "RegistryMixinBase" and bases:
mcls.registry[name] = cls
return cls
class RegistryMixinBase(metaclass=RegistryMeta):
"""Inherit this to be auto-registered."""
class Alpha(RegistryMixinBase):
pass
class Beta(RegistryMixinBase):
pass
print(sorted(RegistryMeta.registry)) # ['Alpha', 'Beta'] - both registered
# no class body, no register() call - the metaclass saw the creation
A metaclass is a subclass of type, and its __new__/__call__ run at class-creation time with the name, bases, and namespace in hand. The metaclass=RegistryMeta keyword on the base tells every inheriting class to be built by RegistryMeta, so Alpha and Beta register themselves by existing. The super().__new__ call does the real construction; the metaclass wraps it to add behavior. The three hooks to know: __new__ (create and return the class object - where structural checks and modifications belong), __init__ (initialize the already-created class object), and __call__ (what runs when someone calls the class - i.e. when an instance is made; this is where instance-creation policy lives). Most metaclasses touch only __new__.
Enforcing Contracts at Definition Time
class ApiMeta(type):
REQUIRED = ("name", "version", "endpoints")
def __new__(mcls, name, bases, namespace, **kwargs):
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
if bases: # skip the base itself
missing = [a for a in mcls.REQUIRED if a not in namespace]
if missing:
raise TypeError(
f"{name} must define {missing}")
return cls
class PluginBase(metaclass=ApiMeta):
name = "base"
version = "0"
endpoints = ()
class Good(PluginBase):
name = "auth"
version = "1"
endpoints = ("/login",)
class Bad(PluginBase):
name = "broken"
# version and endpoints missing
# Good() defines; Bad() raises TypeError AT CLASS DEFINITION
try:
class Bad2(PluginBase):
name = "broken2"
except TypeError as e:
print(e) # Bad2 must define ['version', 'endpoints']
This is the metaclass's most common legitimate job: a contract that must hold for every subclass, checked the moment the subclass is defined - earlier and more precisely than an ABC's instantiation-time check. The ABC says "you cannot make an instance until you implement the methods"; the metaclass says "you cannot even define the class until you declare the attributes." The error at definition time is the better failure mode for a plugin system - the broken plugin is caught when its module is imported, not when the first request hits it. The cost is the same as any metaclass: the logic runs for every class in the hierarchy, and it is harder to read than a base-class check.
The Lighter Alternatives
# __init_subclass__: a classmethod hook, NO metaclass needed
class TrackedBase:
_all: list[type] = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
TrackedBase._all.append(cls) # every subclass records itself
class A(TrackedBase): pass
class B(TrackedBase): pass
print([c.__name__ for c in TrackedBase._all]) # ['A', 'B']
# a class decorator: modify the class after it is built
def freeze(cls):
cls.__setattr__ = lambda self, k, v: (_ for _ in ()).throw(
AttributeError(f"{cls.__name__} is frozen"))
return cls
@freeze
class Config:
def __init__(self):
self.x = 1
Two of the three metaclass jobs have lighter tools. __init_subclass__ is a classmethod that runs automatically when a class is subclassed - it receives the new class and can register it, check it, or configure it, with no metaclass in the MRO. It is the right tool for auto-registration and per-subclass setup, which is most of what people actually use metaclasses for. A class decorator runs once on the built class and can modify its namespace - the right tool for adding methods or enforcing a one-time transformation. The decision ladder: need to run at subclass time? __init_subclass__. Need to transform the class once? a decorator. Need to control class creation itself, or instance creation via __call__, or compose with another metaclass? a metaclass. Climb the ladder only as far as the job requires - a metaclass where __init_subclass__ would do is complexity the reader must pay for every class in the tree.
The Canonical Example: ORM Base Classes
Object-relational mappers (SQLAlchemy, the older Django-adjacent ORMs, and Peewee's own model system) are the standard real-world metaclass application. The model base class uses a metaclass that, at each model class's definition, collects the declared field attributes, turns them into column descriptors, builds the table schema, and wires up the query API - all before the first instance exists. The user writes a plain class User(Model): name = StringField() and the metaclass does the machinery. The reason it must be a metaclass (not __init_subclass__) is that the transformation is deep: it rewrites the class's namespace, creates descriptor objects, and sometimes needs to control how the class is called. When your design reaches that depth - "declaring a class should build a machine" - the metaclass is the right tool, and the ORM base is the template for reading one.
Summary
A metaclass is the class that creates classes: type(name, bases, namespace) is what the class statement compiles to, and subclassing type lets you intercept that creation in __new__ to inspect, modify, or reject the class. The legitimate uses are narrow - enforcing a contract at definition time (earlier than an ABC's instantiation check), auto-registering subclasses, and the deep namespace rewrites of an ORM base class. Before reaching for one, climb the ladder: __init_subclass__ for subclass-time hooks, a class decorator for a one-time transformation, and a metaclass only when you need to control creation itself or instance construction via __call__. It is a framework mechanism, not a default - used well it is how a declarative API is built, used by habit it is a tax on every class in the hierarchy.