Python Metaclasses Explained: Understanding the Class of a Class
Demystify Python metaclasses with practical examples. Learn how classes are created, when to use metaclasses, and real-world patterns.
What you'll learn
- ✓What metaclasses are and how Python uses them
- ✓How class creation works under the hood
- ✓Building custom metaclasses for validation and registration
- ✓When to use metaclasses vs simpler alternatives
Prerequisites
- •Strong Python OOP knowledge
- •Understanding of class inheritance
- •Familiarity with dunder methods
The Big Idea
In Python, everything is an object — including classes. If a class is an object, then something must create it. That something is a metaclass. By default, the metaclass of every class is type. A metaclass is, quite literally, the class of a class.
class Dog:
pass
print(type(Dog)) # <class 'type'>
print(type(Dog())) # <class '__main__.Dog'>
Dog is an instance of type. When you write class Dog: pass, Python calls type('Dog', (object,), {}) behind the scenes to create the Dog class object.
How Classes Are Actually Created
When Python encounters a class statement, it goes through these steps:
- The class body is executed as a code block, collecting attributes into a dictionary.
- Python determines the metaclass (default is
type). - The metaclass is called with three arguments: name, bases, and the namespace dictionary.
You can replicate class creation manually:
# These two are equivalent:
# Standard syntax
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x}, {self.y})"
# Manual creation with type()
def init(self, x, y):
self.x = x
self.y = y
def repr_(self):
return f"Point({self.x}, {self.y})"
Point = type('Point', (object,), {'__init__': init, '__repr__': repr_})
Both produce identical class objects.
Writing Your First Metaclass
A metaclass is a class that inherits from type and overrides __new__ or __init__:
class ValidatedMeta(type):
"""Metaclass that enforces all methods have docstrings."""
def __new__(mcs, name, bases, namespace):
for attr_name, attr_value in namespace.items():
if callable(attr_value) and not attr_name.startswith('_'):
if not attr_value.__doc__:
raise TypeError(
f"Method '{attr_name}' in class '{name}' "
f"must have a docstring"
)
return super().__new__(mcs, name, bases, namespace)
class MyService(metaclass=ValidatedMeta):
def process(self):
"""Process the data."""
pass
def validate(self):
"""Validate inputs."""
pass
# This would raise TypeError:
# class BadService(metaclass=ValidatedMeta):
# def handle(self): # No docstring!
# pass
The __new__ method in a metaclass is called when the class is being created, not when instances of the class are created. This lets you inspect, modify, or reject class definitions at creation time.
Practical Pattern: Automatic Registration
One of the most useful metaclass patterns is automatic registration of subclasses:
class PluginMeta(type):
"""Metaclass that registers all subclasses in a registry."""
registry = {}
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
# Don't register the base class itself
if bases:
plugin_name = namespace.get('name', name.lower())
mcs.registry[plugin_name] = cls
return cls
class Plugin(metaclass=PluginMeta):
"""Base class for all plugins."""
name = None
def execute(self):
raise NotImplementedError
class CSVExporter(Plugin):
name = "csv"
def execute(self):
print("Exporting to CSV")
class JSONExporter(Plugin):
name = "json"
def execute(self):
print("Exporting to JSON")
# All plugins are registered automatically
print(PluginMeta.registry)
# {'csv': <class 'CSVExporter'>, 'json': <class 'JSONExporter'>}
def get_exporter(name):
return PluginMeta.registry[name]()
Every time a new Plugin subclass is defined anywhere in the codebase, it is automatically registered. No manual registration step is needed.
Practical Pattern: Singleton via Metaclass
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):
self.connection = "connected"
print("Database initialized")
db1 = Database() # Prints "Database initialized"
db2 = Database() # No print -- returns existing instance
print(db1 is db2) # True
Here we override __call__ instead of __new__. The metaclass __call__ is invoked when you call the class (i.e., when creating instances), while __new__ is invoked when creating the class itself.
init_subclass: The Simpler Alternative
Python 3.6 introduced __init_subclass__, which handles many cases that previously required metaclasses:
class Plugin:
registry = {}
def __init_subclass__(cls, name=None, **kwargs):
super().__init_subclass__(**kwargs)
plugin_name = name or cls.__name__.lower()
Plugin.registry[plugin_name] = cls
class CSVExporter(Plugin, name="csv"):
pass
class JSONExporter(Plugin, name="json"):
pass
print(Plugin.registry)
# {'csv': <class 'CSVExporter'>, 'json': <class 'JSONExporter'>}
This achieves the same registration pattern without a metaclass. Prefer __init_subclass__ when it meets your needs.
When Should You Actually Use Metaclasses?
Metaclasses are powerful but rarely necessary. Use them when:
- You need to modify the class creation process itself (not just subclass behavior).
- You need to enforce constraints across all classes using the metaclass.
- You are building a framework where users define classes that must follow specific rules (like Django models or SQLAlchemy).
Do not use metaclasses when:
__init_subclass__can do the job.- Class decorators can achieve the same result.
- Simple inheritance solves the problem.
# Often a class decorator is simpler than a metaclass
def add_logging(cls):
original_init = cls.__init__
def new_init(self, *args, **kwargs):
print(f"Creating {cls.__name__}")
original_init(self, *args, **kwargs)
cls.__init__ = new_init
return cls
@add_logging
class MyClass:
def __init__(self, value):
self.value = value
The Metaclass Resolution Order
When a class has multiple bases with different metaclasses, Python must find a single metaclass that is a subclass of all of them. If no such metaclass exists, you get a TypeError:
class MetaA(type): pass
class MetaB(type): pass
class A(metaclass=MetaA): pass
class B(metaclass=MetaB): pass
# This fails:
# class C(A, B): pass
# TypeError: metaclass conflict
# Fix by creating a combined metaclass:
class MetaC(MetaA, MetaB): pass
class C(A, B, metaclass=MetaC): pass
Wrapping Up
Metaclasses are the mechanism behind Python’s class creation. They power frameworks like Django and SQLAlchemy, enable automatic registration patterns, and enforce coding standards at the class level. However, with __init_subclass__ and class decorators available, you should reach for metaclasses only when simpler tools fall short. Understanding them deeply, even if you rarely write them, will make you a better Python developer.
Related articles
- Python Python Descriptors: The Protocol Behind Properties
Learn how Python descriptors work under the hood, powering properties, class methods, and custom attribute access with __get__, __set__, and __delete__.
- Python Python ABCs vs Protocols: Choosing the Right Abstraction
Understand the difference between Abstract Base Classes and Protocols in Python. Learn when nominal typing beats structural typing and vice versa.
- Python Python Design Patterns: Strategy, Factory, and Observer
Learn how to implement the Strategy, Factory, and Observer design patterns in Python using idiomatic constructs like first-class functions and protocols.
- Python Advanced Python Dataclasses: Beyond the Basics
Go beyond basic dataclass usage with post-init processing, field factories, inheritance, frozen instances, and custom serialization.