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.

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
Prefer __init_subclass__: For most use cases — registries, validation, auto-configuration — __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 conflict if two bases have incompatible metaclasses. The solution is to create a merged metaclass: class CombinedMeta(Meta1, Meta2): pass.