Python Metaclasses: Advanced Class Customization
In Python, everything is an object — including classes. A metaclass is the class of a class: it controls how a class is created, what attributes it has, and how it behaves at the class level. While most Python developers never need to write a metaclass directly, understanding them reveals how frameworks like Django ORM, SQLAlchemy, and Pydantic work under the hood. They enable powerful patterns like automatic registration, interface enforcement, and DSL creation.
Table of Contents
type() and the Class Creation Process
When Python executes a class statement, it calls the metaclass (by default type) with three arguments: the class name, its base classes, and a namespace dictionary of its attributes. Understanding this process is the key to metaclass programming. type() is both a function (returns the type of an object) and the default metaclass (creates new class objects).
# type() as a function
print(type(42)) #
print(type("hello")) #
print(type(int)) # ← int's metaclass is type
print(type(type)) # ← type is its own metaclass
# type() as a class factory: type(name, bases, namespace)
Dog = type("Dog", (object,), {
"species": "Canis lupus familiaris",
"speak": lambda self: "Woof!",
"__init__": lambda self, name: setattr(self, "name", name),
})
rex = Dog("Rex")
print(rex.speak()) # Woof!
print(rex.species) # Canis lupus familiaris
# Equivalent class statement:
class Dog:
species = "Canis lupus familiaris"
def __init__(self, name):
self.name = name
def speak(self):
return "Woof!"
# Class creation order:
# 1. Python executes the class body as a function, collecting namespace
# 2. Python determines the metaclass (explicit metaclass= or inherited)
# 3. Python calls metaclass(name, bases, namespace)
# 4. The resulting object is bound to the class name
Writing a Custom Metaclass
A custom metaclass inherits from type and overrides __new__ (called when the class object is created) and/or __init__ (called after creation) to customize class behavior. __new__ is where you can modify the class namespace, rename attributes, enforce conventions, or add methods to every class that uses the metaclass.
class EnforceDocstrings(type):
"""Metaclass that requires docstrings on all public methods."""
def __new__(mcs, name, bases, namespace):
# Check all public methods have docstrings
for attr_name, attr_value in namespace.items():
if callable(attr_value) and not attr_name.startswith("_"):
if not getattr(attr_value, "__doc__", None):
raise TypeError(
f"{name}.{attr_name}() is missing a docstring"
)
return super().__new__(mcs, name, bases, namespace)
class ServiceBase(metaclass=EnforceDocstrings):
pass
# This works:
class PaymentService(ServiceBase):
def charge(self, amount: float) -> bool:
"""Charge the customer for the given amount in USD."""
return True
# This raises TypeError at class definition time:
# class BrokenService(ServiceBase):
# def process(self): # missing docstring!
# pass
# Singleton metaclass
class SingletonMeta(type):
_instances: dict = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Config(metaclass=SingletonMeta):
def __init__(self):
self.debug = False
self.db_url = "postgresql://localhost/app"
c1 = Config()
c2 = Config()
print(c1 is c2) # True — same instance
__init_subclass__: The Modern Alternative
Python 3.6 introduced __init_subclass__, a class method called automatically on a base class whenever a subclass is defined. This handles most use cases that previously required metaclasses — validation, registration, and configuration — with much simpler syntax. If you find yourself reaching for a metaclass, consider __init_subclass__ first.
class Plugin:
"""Base class with automatic subclass registration."""
_registry: dict[str, type] = {}
def __init_subclass__(cls, plugin_name: str = None, **kwargs):
super().__init_subclass__(**kwargs)
name = plugin_name or cls.__name__.lower()
Plugin._registry[name] = cls
print(f"Registered plugin: {name}")
class JSONPlugin(Plugin, plugin_name="json"):
def serialize(self, data): return __import__('json').dumps(data)
class XMLPlugin(Plugin, plugin_name="xml"):
def serialize(self, data): return f"{data}"
print(Plugin._registry)
# {'json': , 'xml': }
# Use case: validator enforcement
class ValidatedModel:
"""Ensure all subclasses define required class attributes."""
_required_attrs: tuple[str, ...] = ()
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
for attr in cls._required_attrs:
if not hasattr(cls, attr):
raise TypeError(f"{cls.__name__} must define '{attr}'")
class APIEndpoint(ValidatedModel):
_required_attrs = ("path", "method")
# Works fine:
class UserEndpoint(APIEndpoint):
path = "/users"
method = "GET"
# Raises TypeError at definition time:
# class BrokenEndpoint(APIEndpoint):
# path = "/broken"
# # missing 'method'!
ABCMeta and Abstract Base Classes
The abc module uses ABCMeta as its metaclass to implement abstract methods. Classes inheriting from ABC (or using ABCMeta directly) cannot be instantiated if they have unimplemented abstract methods. This enables Python's version of interfaces — enforced structural contracts with clear error messages at instantiation time.
from abc import ABC, abstractmethod, abstractproperty
from typing import Protocol
class Repository(ABC):
"""Abstract repository interface."""
@abstractmethod
def find_by_id(self, id: int) -> dict | None:
"""Find a single record by primary key."""
...
@abstractmethod
def find_all(self, limit: int = 100) -> list[dict]:
"""Return all records up to limit."""
...
@abstractmethod
def save(self, entity: dict) -> dict:
"""Persist an entity and return it with generated ID."""
...
@abstractmethod
def delete(self, id: int) -> bool:
"""Delete a record by ID. Return True if found and deleted."""
...
# Concrete method in an abstract class — shared implementation
def find_or_raise(self, id: int) -> dict:
result = self.find_by_id(id)
if result is None:
raise KeyError(f"Entity {id} not found")
return result
# Concrete implementation
class InMemoryUserRepository(Repository):
def __init__(self):
self._store: dict[int, dict] = {}
self._next_id = 1
def find_by_id(self, id: int) -> dict | None:
return self._store.get(id)
def find_all(self, limit: int = 100) -> list[dict]:
return list(self._store.values())[:limit]
def save(self, entity: dict) -> dict:
if "id" not in entity:
entity["id"] = self._next_id
self._next_id += 1
self._store[entity["id"]] = entity
return entity
def delete(self, id: int) -> bool:
return self._store.pop(id, None) is not None
# repo = Repository() # TypeError: Can't instantiate abstract class
repo = InMemoryUserRepository() # Works fine
Class Decorators
Class decorators are functions that receive a class and return a modified class. They are simpler than metaclasses for most use cases: adding methods, modifying attributes, or wrapping the class in a proxy. Python's standard library uses class decorators extensively — @dataclass, @functools.total_ordering, and @enum.unique are all class decorators.
import functools
import time
def add_repr(cls):
"""Decorator: add a generic __repr__ based on __dict__."""
def __repr__(self):
attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
return f"{cls.__name__}({attrs})"
cls.__repr__ = __repr__
return cls
def track_instances(cls):
"""Decorator: track all instances of a class."""
cls._instances = []
original_init = cls.__init__
@functools.wraps(original_init)
def __init__(self, *args, **kwargs):
original_init(self, *args, **kwargs)
cls._instances.append(self)
cls.__init__ = __init__
@classmethod
def get_all(klass):
return list(klass._instances)
cls.get_all = get_all
return cls
@add_repr
@track_instances
class Connection:
def __init__(self, host: str, port: int):
self.host = host
self.port = port
c1 = Connection("db.techoral.com", 5432)
c2 = Connection("cache.techoral.com", 6379)
print(c1) # Connection(host='db.techoral.com', port=5432)
print(Connection.get_all()) # [c1, c2]
Registry Pattern with Metaclasses
The registry pattern uses metaclasses or __init_subclass__ to automatically build a mapping from names to classes. This enables plugin systems, command dispatchers, and serialization frameworks where new types are discovered by convention rather than explicit registration — the same approach used by Django's model registry and Click's command groups.
class CommandMeta(type):
"""Metaclass that builds a command registry."""
registry: dict[str, type] = {}
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
# Register non-base classes
if bases:
cmd_name = namespace.get("name") or name.lower().replace("command", "")
mcs.registry[cmd_name] = cls
return cls
class Command(metaclass=CommandMeta):
"""Base command class."""
name: str = None
def execute(self, args: list[str]) -> int:
raise NotImplementedError
class BuildCommand(Command):
name = "build"
def execute(self, args): print(f"Building: {args}"); return 0
class DeployCommand(Command):
name = "deploy"
def execute(self, args): print(f"Deploying to {args}"); return 0
class TestCommand(Command):
name = "test"
def execute(self, args): print(f"Testing: {args}"); return 0
# Dispatch by name — no if/elif chain needed
def run_command(name: str, args: list[str]) -> int:
cmd_cls = CommandMeta.registry.get(name)
if not cmd_cls:
print(f"Unknown command: {name}")
return 1
return cmd_cls().execute(args)
run_command("build", ["--release"])
run_command("deploy", ["--env", "prod"])
print(CommandMeta.registry.keys()) # dict_keys(['build', 'deploy', 'test'])
ORM-Style Descriptor Pattern
Descriptors combined with metaclasses enable ORM-style field definitions where class-level attributes become column definitions at class creation time. This is exactly how Django models, SQLAlchemy mapped classes, and Pydantic models work. The metaclass collects fields from the class namespace and stores them separately, leaving instances clean.
class Field:
"""Base descriptor for ORM-style field definitions."""
def __init__(self, field_type: type, required: bool = True, default=None):
self.field_type = field_type
self.required = required
self.default = default
self.name = None # set by ModelMeta
def __set_name__(self, owner, name):
self.name = name
self.private_name = f"_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private_name, self.default)
def __set__(self, obj, value):
if value is not None and not isinstance(value, self.field_type):
raise TypeError(f"{self.name}: expected {self.field_type.__name__}, got {type(value).__name__}")
setattr(obj, self.private_name, value)
class ModelMeta(type):
def __new__(mcs, name, bases, namespace):
fields = {}
for attr_name, attr_value in list(namespace.items()):
if isinstance(attr_value, Field):
fields[attr_name] = attr_value
namespace["_fields"] = fields
return super().__new__(mcs, name, bases, namespace)
class Model(metaclass=ModelMeta):
def __init__(self, **kwargs):
for name, field in self._fields.items():
value = kwargs.get(name, field.default)
if field.required and value is None:
raise ValueError(f"{name} is required")
setattr(self, name, value)
def to_dict(self):
return {name: getattr(self, name) for name in self._fields}
class User(Model):
name = Field(str)
age = Field(int)
email = Field(str, required=False, default="")
user = User(name="Alice", age=30, email="alice@techoral.com")
print(user.to_dict()) # {'name': 'Alice', 'age': 30, 'email': 'alice@techoral.com'}
__init_subclass__ or class decorators. Both approaches are simpler, more explicit, and easier to test.