Python Metaclasses: Advanced Class Customization
In Python, everything is an object — including classes. A metaclass is the class of a class: it controls how classes are created, just as a class controls how instances are created. Understanding metaclasses unlocks the ability to build ORMs, API routers, validation frameworks, and plugin systems that feel like magic from the outside but follow clear Python rules internally.
Table of Contents
type: The Default Metaclass
Every class in Python is an instance of its metaclass. By default that metaclass is type. When Python executes a class statement, it calls type(name, bases, namespace) to create the class object. You can call type directly to create classes dynamically.
# These two definitions are equivalent
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def distance(self):
return (self.x**2 + self.y**2) ** 0.5
Point2 = type(
"Point2", # class name
(object,), # base classes
{ # class namespace
"__init__": lambda self, x, y: setattr(self, 'x', x) or setattr(self, 'y', y),
"distance": lambda self: (self.x**2 + self.y**2) ** 0.5,
}
)
# Verify the metaclass
print(type(Point)) #
print(type(int)) #
print(type(type)) # — type is its own metaclass
print(isinstance(Point, type)) # True
Writing a Custom Metaclass
Create a custom metaclass by subclassing type and overriding __new__ or __init__. __new__ receives the namespace dict and returns the new class object; __init__ is called after with the same arguments. Override __new__ to modify the class dict before the class is created; override __init__ to perform post-creation setup.
class SingletonMeta(type):
"""Metaclass that makes a class a Singleton."""
_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
print(f"Connecting to {url}")
db1 = Database("postgres://localhost/mydb") # prints connection message
db2 = Database("postgres://localhost/other") # returns same instance
assert db1 is db2 # True
# Metaclass that auto-registers subclasses
class PluginMeta(type):
registry = {}
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if bases: # skip the base Plugin class itself
plugin_name = namespace.get("name", name.lower())
mcs.registry[plugin_name] = cls
return cls
class Plugin(metaclass=PluginMeta):
name = None
class JSONPlugin(Plugin):
name = "json"
def process(self, data): return json.dumps(data)
class CSVPlugin(Plugin):
name = "csv"
def process(self, data): return ",".join(str(x) for x in data)
print(PluginMeta.registry)
# {'json': , 'csv': }
__init_subclass__: The Simpler Alternative
Python 3.6 introduced __init_subclass__, which handles the most common metaclass use case — reacting when a subclass is defined — without actually writing a metaclass. It is a classmethod on the base class called whenever a new subclass is created. Prefer this over metaclasses when you control the base class.
class Handler:
"""Base class that auto-registers handlers by their 'route' attribute."""
_routes = {}
def __init_subclass__(cls, route=None, **kwargs):
super().__init_subclass__(**kwargs)
if route is not None:
Handler._routes[route] = cls
cls.route = route
class HomeHandler(Handler, route="/"):
def get(self): return "Home page"
class AboutHandler(Handler, route="/about"):
def get(self): return "About page"
print(Handler._routes)
# {'/': , '/about': }
# Enforce required attributes on subclasses
class Model:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if not hasattr(cls, "table_name"):
raise TypeError(f"{cls.__name__} must define 'table_name'")
class UserModel(Model):
table_name = "users" # required — will error without this
# class BadModel(Model): pass # raises TypeError at class definition time
__init_subclass__ is cleaner and more composable than a metaclass. Use metaclasses only when you need to intercept __new__, override attribute access on the class itself, or work with frameworks that already mandate a metaclass (like Django models or SQLAlchemy declarative base).
Class Decorators
Class decorators are another metaclass alternative. They run after the class is created and receive the class object, returning a (possibly modified) class or a replacement. They are simpler than metaclasses and compose better.
import functools
def add_repr(cls):
"""Decorator that generates a __repr__ from annotated fields."""
fields = list(cls.__annotations__.keys())
def __repr__(self):
attrs = ", ".join(f"{f}={getattr(self, f)!r}" for f in fields)
return f"{cls.__name__}({attrs})"
cls.__repr__ = __repr__
return cls
def validate_types(cls):
"""Decorator that adds runtime type checking to __init__."""
original_init = cls.__init__
hints = cls.__annotations__
@functools.wraps(original_init)
def __init__(self, *args, **kwargs):
original_init(self, *args, **kwargs)
for field, expected in hints.items():
value = getattr(self, field, None)
if value is not None and not isinstance(value, expected):
raise TypeError(f"{field} must be {expected.__name__}, got {type(value).__name__}")
cls.__init__ = __init__
return cls
@add_repr
@validate_types
class Product:
name: str
price: float
quantity: int
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
p = Product("Widget", 9.99, 100)
print(p) # Product(name='Widget', price=9.99, quantity=100)
Abstract Base Classes
The abc module uses a metaclass (ABCMeta) under the hood but exposes a clean API via ABC base class and @abstractmethod. Classes with abstract methods cannot be instantiated until all abstract methods are implemented, enforcing interface contracts at class definition time.
from abc import ABC, abstractmethod
class Storage(ABC):
"""Abstract interface for pluggable storage backends."""
@abstractmethod
def get(self, key: str) -> bytes | None: ...
@abstractmethod
def set(self, key: str, value: bytes, ttl: int = 0) -> None: ...
@abstractmethod
def delete(self, key: str) -> bool: ...
def get_json(self, key):
import json
raw = self.get(key)
return json.loads(raw) if raw else None
class RedisStorage(Storage):
def __init__(self, client):
self.client = client
def get(self, key):
return self.client.get(key)
def set(self, key, value, ttl=0):
self.client.set(key, value, ex=ttl or None)
def delete(self, key):
return bool(self.client.delete(key))
# Storage() # raises TypeError: Can't instantiate abstract class
Real-World Metaclass Patterns
Metaclasses power many popular Python libraries. Django's ORM uses a metaclass to inspect field definitions and build SQL schemas. SQLAlchemy's declarative base works the same way. Here is a simplified ORM-style example showing the pattern:
class Field:
def __init__(self, field_type, primary_key=False, nullable=True):
self.field_type = field_type
self.primary_key = primary_key
self.nullable = nullable
class ModelMeta(type):
def __new__(mcs, name, bases, namespace):
fields = {}
for key, value in list(namespace.items()):
if isinstance(value, Field):
fields[key] = value
namespace['_fields'] = fields
namespace['_table'] = name.lower() + 's'
return super().__new__(mcs, name, bases, namespace)
class Model(metaclass=ModelMeta):
def __init__(self, **kwargs):
for field in self._fields:
setattr(self, field, kwargs.get(field))
@classmethod
def create_table_sql(cls):
cols = []
for name, field in cls._fields.items():
col = f"{name} {field.field_type}"
if field.primary_key:
col += " PRIMARY KEY"
if not field.nullable:
col += " NOT NULL"
cols.append(col)
return f"CREATE TABLE {cls._table} ({', '.join(cols)});"
class User(Model):
id = Field("INTEGER", primary_key=True)
username = Field("VARCHAR(50)", nullable=False)
email = Field("VARCHAR(200)", nullable=False)
print(User.create_table_sql())
# CREATE TABLE users (id INTEGER PRIMARY KEY, username VARCHAR(50) NOT NULL, email VARCHAR(200) NOT NULL);
user = User(id=1, username="alice", email="alice@example.com")
print(user.username) # alice
Frequently Asked Questions
- When should I actually use a metaclass?
- When you're building a framework that other developers subclass from, and you need to intercept class creation itself — not just instance creation. Django, SQLAlchemy, and pytest all use metaclasses. Application code almost never needs them.
- What is the difference between __new__ and __init__ in a metaclass?
__new__creates and returns the class object.__init__initializes it after creation. Most metaclass logic goes in__new__because you can modify the namespace dict before the class is built.- Can a class have multiple metaclasses?
- No directly — Python raises
TypeError: metaclass conflictif two bases have incompatible metaclasses. The solution is to create a merged metaclass:class CombinedMeta(Meta1, Meta2): pass.