Metaclasses Advanced¶
When you'd use this
type, new, init_subclass, prepare and controlling class creation.
Customize how classes themselves are built — used by ORMs, serializers, plugin registries, and frameworks that auto-wire classes. Rarely needed in app code.
The fundamental truth: classes are objects¶
The fundamental truth: classes are objects — a key concept in Metaclasses.
In Python, everything is an object — including classes. A class is an instance of its metaclass.
class Dog:
pass
print(type(Dog)) # <class 'type'>
print(type(Dog())) # <class '__main__.Dog'>
# type(instance) → class
# type(class) → metaclass
# type(type) → type (it's its own metaclass!)
Creating classes dynamically with type()¶
type(name, bases, namespace) builds a class at runtime — classes are objects.
type has a dual role — as a function it returns the type of an object; with three arguments it creates a new class:
# type(name, bases, namespace)
def bark(self):
return f"{self.name} says Woof!"
Dog = type("Dog", (), {
"species": "Canis familiaris",
"bark": bark,
"__init__": lambda self, name: setattr(self, "name", name),
})
rex = Dog("Rex")
print(rex.bark()) # Rex says Woof!
print(Dog.species) # Canis familiaris
print(type(rex)) # <class '__main__.Dog'>
print(type(Dog)) # <class 'type'>
This is exactly what the class statement does under the hood.
How class creation works (step by step)¶
What Python does between class X: and the finished class object.
When Python encounters:
It executes these steps:
# 1. Determine metaclass
metaclass = Meta # (or type if not specified)
# 2. Prepare the namespace
namespace = metaclass.__prepare__("Foo", (Base,)) # usually returns dict
# 3. Execute the class body inside that namespace
exec(body, globals(), namespace)
# Now namespace = {"x": 10, "method": <function>}
# 4. Call the metaclass to create the class object
Foo = metaclass("Foo", (Base,), namespace)
# This calls metaclass.__new__ then metaclass.__init__
Writing a custom metaclass¶
Subclass type and override __new__/__init__ to intercept and customize class creation — the mechanism behind ORMs, serializers, and plugin registries. Use __init_subclass__ instead when you only need to react to subclassing.
class Meta(type):
"""Metaclass that prints when classes are created."""
def __new__(mcs, name, bases, namespace):
print(f" Creating class: {name}")
print(f" Bases: {bases}")
print(f" Attributes: {list(namespace.keys())}")
cls = super().__new__(mcs, name, bases, namespace)
return cls
def __init__(cls, name, bases, namespace):
print(f" Initializing class: {name}")
super().__init__(name, bases, namespace)
class Animal(metaclass=Meta):
sound = "..."
# Output:
# Creating class: Animal
# Bases: ()
# Attributes: ['__module__', '__qualname__', 'sound']
# Initializing class: Animal
class Dog(Animal):
sound = "Woof"
# Output:
# Creating class: Dog
# Bases: (<class 'Animal'>,)
# Attributes: ['__module__', '__qualname__', 'sound']
# Initializing class: Dog
__new__ vs __init__ in metaclasses¶
__new__ builds the class; __init__ configures it afterward.
| Method | Called on | Purpose |
|---|---|---|
Meta.__new__(mcs, name, bases, ns) | The metaclass | Creates the class object |
Meta.__init__(cls, name, bases, ns) | The new class | Initializes the class after creation |
Meta.__call__(cls, *args, **kwargs) | The class | Called when you instantiate Foo() |
class SingletonMeta(type):
"""Each class created with this metaclass has at most one instance."""
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
# Call type.__call__ which calls cls.__new__ + cls.__init__
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class Database(metaclass=SingletonMeta):
def __init__(self, url="sqlite:///app.db"):
self.url = url
print(f" Connecting to {url}")
db1 = Database("postgres://localhost/mydb")
# Output: Connecting to postgres://localhost/mydb
db2 = Database("mysql://other")
# No output — same instance returned
print(db1 is db2) # True
print(db2.url) # postgres://localhost/mydb (first call's value)
__prepare__ — controlling the namespace¶
Customize the mapping used to collect the class body (e.g. ordered or validating).
__prepare__ returns the dict-like object used for the class body. You can use this to customize how the body is evaluated.
from collections import OrderedDict
class OrderedMeta(type):
@classmethod
def __prepare__(mcs, name, bases):
return OrderedDict()
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, dict(namespace))
cls._fields = [k for k in namespace if not k.startswith('_')]
return cls
class Record(metaclass=OrderedMeta):
name = ""
age = 0
city = ""
print(Record._fields) # ['name', 'age', 'city'] — preserves definition order
Python 3.7+ dicts are ordered
Since Python 3.7, regular dicts maintain insertion order, so __prepare__ returning OrderedDict is less necessary. But the mechanism is still useful for other things like logging or validating definitions.
Use Case: Auto-registering plugins¶
Use Case: Auto-registering plugins in Metaclasses — what it is and when to use it.
class PluginMeta(type):
registry = {}
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
# Don't register the base class itself
if bases:
mcs.registry[name] = cls
return cls
class Plugin(metaclass=PluginMeta):
"""Base class for all plugins."""
pass
class PDFExporter(Plugin):
def export(self, data):
return f"PDF: {data}"
class CSVExporter(Plugin):
def export(self, data):
return f"CSV: {data}"
class JSONExporter(Plugin):
def export(self, data):
return f"JSON: {data}"
print(PluginMeta.registry)
# {'PDFExporter': <class 'PDFExporter'>,
# 'CSVExporter': <class 'CSVExporter'>,
# 'JSONExporter': <class 'JSONExporter'>}
# Dynamic plugin loading
def get_exporter(name):
return PluginMeta.registry[name]()
exporter = get_exporter("PDFExporter")
print(exporter.export("hello")) # PDF: hello
Use Case: Interface enforcement¶
Use Case: Interface enforcement in Metaclasses — what it is and when to use it.
class InterfaceMeta(type):
"""Metaclass that enforces subclasses implement required methods."""
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
# Skip the base class
if bases:
required = getattr(cls, '_required_methods', [])
for method_name in required:
if method_name not in namespace:
raise TypeError(
f"Class '{name}' must implement '{method_name}'"
)
return cls
class Serializer(metaclass=InterfaceMeta):
_required_methods = ['serialize', 'deserialize']
# This works:
class JSONSerializer(Serializer):
def serialize(self, obj):
import json
return json.dumps(obj)
def deserialize(self, data):
import json
return json.loads(data)
# This raises TypeError at class DEFINITION time (not instantiation):
try:
class BrokenSerializer(Serializer):
def serialize(self, obj):
return str(obj)
# Missing deserialize!
except TypeError as ex:
print(ex) # Class 'BrokenSerializer' must implement 'deserialize'
Use Case: Automatic __repr__ and __eq__¶
Use Case: Automatic repr and eq in Metaclasses — what it is and when to use it.
class AutoMeta(type):
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
# Find annotated fields
annotations = namespace.get('__annotations__', {})
if annotations:
fields = list(annotations.keys())
def __repr__(self):
values = ", ".join(f"{f}={getattr(self, f)!r}" for f in fields)
return f"{name}({values})"
def __eq__(self, other):
if type(self) != type(other):
return NotImplemented
return all(getattr(self, f) == getattr(other, f) for f in fields)
cls.__repr__ = __repr__
cls.__eq__ = __eq__
return cls
class Point(metaclass=AutoMeta):
x: float
y: float
def __init__(self, x, y):
self.x = x
self.y = y
p1 = Point(3.0, 4.0)
p2 = Point(3.0, 4.0)
p3 = Point(1.0, 2.0)
print(p1) # Point(x=3.0, y=4.0)
print(p1 == p2) # True
print(p1 == p3) # False
This is essentially what @dataclass does
The @dataclass decorator uses similar introspection (though it's a decorator, not a metaclass).
__init_subclass__ — the simpler alternative (Python 3.6+)¶
React to subclassing without a full metaclass — covers most real needs.
For most metaclass use cases, __init_subclass__ is simpler and sufficient:
class Plugin:
_registry = {}
def __init_subclass__(cls, plugin_name=None, **kwargs):
super().__init_subclass__(**kwargs)
name = plugin_name or cls.__name__
cls._registry[name] = cls
class PDF(Plugin, plugin_name="pdf"):
pass
class CSV(Plugin, plugin_name="csv"):
pass
print(Plugin._registry)
# {'pdf': <class 'PDF'>, 'csv': <class 'CSV'>}
When to use __init_subclass__ vs metaclass:
| Need | Use |
|---|---|
| Register subclasses | __init_subclass__ |
| Validate class at creation time | __init_subclass__ |
| Modify class namespace before execution | Metaclass + __prepare__ |
Control instantiation (__call__) | Metaclass |
| Change how attributes are stored | Metaclass |
Metaclass conflicts and resolution¶
Why mixing metaclasses can clash and how Python resolves it.
class MetaA(type): pass
class MetaB(type): pass
class A(metaclass=MetaA): pass
class B(metaclass=MetaB): pass
# This raises TypeError:
try:
class C(A, B): pass
except TypeError as ex:
print(ex)
# metaclass conflict: the metaclass of a derived class must be a
# (non-strict) subclass of the metaclasses of all its bases
# Solution: create a combined metaclass
class MetaC(MetaA, MetaB): pass
class C(A, B, metaclass=MetaC): pass # works!
Real-world metaclass usage¶
ORMs, serializers, and plugin registries that rely on metaclasses.
| Library | What it does with metaclasses |
|---|---|
| Django ORM | Model uses ModelBase metaclass to collect field definitions |
| SQLAlchemy (classic) | DeclarativeMeta maps classes to database tables |
| Enum | EnumMeta controls how enum members are created |
| ABCMeta | ABCMeta tracks abstract methods |
| Protocol (typing) | Uses metaclass for structural type checking |
When NOT to use metaclasses¶
A core question explored in Metaclasses: When NOT to use metaclasses.
Metaclasses are powerful but complex
Before reaching for a metaclass, try these simpler alternatives:
- Class decorators — modify a class after creation
__init_subclass__— hook into subclass creation- Descriptors — control attribute access
__set_name__— configure descriptors at class creation
Use metaclasses only when you need to: - Control the namespace before class body executes (__prepare__) - Intercept instantiation across all subclasses (__call__) - Do something that can't be done after the class exists
Practice Exercises¶
- Write a
FrozenMetathat makes all attributes read-only after__init__completes. - Write a
ValidatedMetathat checks all methods have type annotations. - Implement the Singleton pattern using a metaclass
__call__. - Write an
AbstractMetathat raisesTypeErrorif a class with abstract methods is instantiated (reimplementABCMeta). - Write a
TrackedMetathat counts how many instances of each class exist. - Use
__prepare__to detect duplicate method definitions in a class body.
💬 Discussion
Have a question about this topic? Found an error? Share your thoughts below.