Python — Metaclasses
A metaclass is a class whose instances are classes — it is the class of a class. Just as an ordinary class defines how its instances behave, a metaclass defines how classes themselves behave. Metaclasses are the deepest metaprogramming tool in Python and should be used sparingly.
Every class in Python is an instance of a metaclass. By default, that metaclass is type. When you write class Foo: ..., Python calls type("Foo", (object,), ) behind the scenes. Metaclasses let you intercept and customize this class-creation process.
best practice
The type() function has two distinct roles. With one argument it returns the type of an object. With three arguments it creates a new class dynamically — the same operation that the class keyword performs at runtime.
| 1 | # type(obj) — get the type |
| 2 | print(type(42)) # → <class 'int'> |
| 3 | print(type("hello")) # → <class 'str'> |
| 4 | print(type(int)) # → <class 'type'> |
| 5 | |
| 6 | # type(name, bases, namespace) — create a class dynamically |
| 7 | MyClass = type("MyClass", (object,), {"x": 10, "greet": lambda self: "hi"}) |
| 1 | # These two are equivalent: |
| 2 | |
| 3 | # 1. class statement |
| 4 | class Foo: |
| 5 | bar = 1 |
| 6 | |
| 7 | def __init__(self): |
| 8 | self.value = 42 |
| 9 | |
| 10 | # 2. type() call |
| 11 | def foo_init(self): |
| 12 | self.value = 42 |
| 13 | |
| 14 | Foo = type("Foo", (object,), {"bar": 1, "__init__": foo_init}) |
The three arguments to type() are: the class name as a string, a tuple of base classes, and a dictionary of attributes and methods. Python's class keyword is syntactic sugar over this exact call.
To create a custom metaclass, inherit from type and override its class-creation hooks. The metaclass is specified via the metaclass= keyword argument in the class definition.
| 1 | # A metaclass must inherit from type |
| 2 | class UppercaseAttributes(type): |
| 3 | """Convert all attribute names to uppercase.""" |
| 4 | |
| 5 | def __new__(mcs, name, bases, namespace): |
| 6 | upper_ns = {k.upper(): v for k, v in namespace.items()} |
| 7 | return super().__new__(mcs, name, bases, upper_ns) |
| 8 | |
| 9 | # Apply it via the metaclass keyword |
| 10 | class MyClass(metaclass=UppercaseAttributes): |
| 11 | foo = 1 |
| 12 | bar = 2 |
| 13 | |
| 14 | print(hasattr(MyClass, "foo")) # → False |
| 15 | print(hasattr(MyClass, "FOO")) # → True |
| 16 | print(hasattr(MyClass, "bar")) # → False |
| 17 | print(hasattr(MyClass, "BAR")) # → True |
The metaclass receives the class name, base classes, and namespace dictionary before the class object is created. This is the interception point where you can validate, modify, or replace the class being defined.
| 1 | # Inheritance works naturally with metaclasses |
| 2 | class ValidateFields(type): |
| 3 | def __new__(mcs, name, bases, namespace): |
| 4 | # Skip base class itself |
| 5 | if name == "BaseModel": |
| 6 | return super().__new__(mcs, name, bases, namespace) |
| 7 | |
| 8 | # Require every field to have a default value |
| 9 | for key, value in namespace.items(): |
| 10 | if key.startswith("_"): |
| 11 | continue |
| 12 | if value is ...: |
| 13 | raise TypeError(f"{name}.{key} requires a default value") |
| 14 | |
| 15 | return super().__new__(mcs, name, bases, namespace) |
| 16 | |
| 17 | class BaseModel(metaclass=ValidateFields): |
| 18 | pass |
| 19 | |
| 20 | class User(BaseModel): |
| 21 | name: str = ... # error — '...' is not a real default |
| 22 | age: int = 0 # ok |
Metaclasses have two main hooks that mirror the instance creation pattern: __new__ and __init__. Both are called every time a class is defined using the metaclass.
| Hook | When It Runs | Purpose | Must Return |
|---|---|---|---|
| __new__ | First | Create and return the class object | The class (or modified class) |
| __init__ | After __new__ | Initialize the class object in-place | Nothing (None) |
| 1 | class TraceMeta(type): |
| 2 | def __new__(mcs, name, bases, namespace): |
| 3 | print(f"[__new__] Creating class {name}") |
| 4 | # __new__ must create and return the class |
| 5 | cls = super().__new__(mcs, name, bases, namespace) |
| 6 | cls._created = True |
| 7 | return cls |
| 8 | |
| 9 | def __init__(cls, name, bases, namespace): |
| 10 | print(f"[__init__] Initializing class {name}") |
| 11 | # __init__ receives the already-created class |
| 12 | # Do not return anything |
| 13 | cls._initialized = True |
| 14 | |
| 15 | class Demo(metaclass=TraceMeta): |
| 16 | pass |
| 17 | # Output: |
| 18 | # [__new__] Creating class Demo |
| 19 | # [__init__] Initializing class Demo |
| 20 | |
| 21 | print(Demo._created) # → True |
| 22 | print(Demo._initialized) # → True |
| 23 | |
| 24 | # __new__ can replace the class entirely: |
| 25 | class SingletonMeta(type): |
| 26 | _instances = {} |
| 27 | |
| 28 | def __call__(cls, *args, **kwargs): |
| 29 | """Intercept instance creation (called when MyClass()).""" |
| 30 | if cls not in cls._instances: |
| 31 | cls._instances[cls] = super().__call__(*args, **kwargs) |
| 32 | return cls._instances[cls] |
info
While rare, metaclasses shine in a few specific patterns. Here are the most common real-world applications.
Singleton Pattern
| 1 | class SingletonMeta(type): |
| 2 | """Metaclass that enforces a single instance.""" |
| 3 | _instances = {} |
| 4 | |
| 5 | def __call__(cls, *args, **kwargs): |
| 6 | if cls not in cls._instances: |
| 7 | cls._instances[cls] = super().__call__(*args, **kwargs) |
| 8 | return cls._instances[cls] |
| 9 | |
| 10 | class Database(metaclass=SingletonMeta): |
| 11 | def __init__(self): |
| 12 | self.connection = self._connect() |
| 13 | |
| 14 | def _connect(self): |
| 15 | print("Connecting to database...") |
| 16 | return "db_handle" |
| 17 | |
| 18 | # Both variables point to the same instance |
| 19 | db1 = Database() # → Connecting to database... |
| 20 | db2 = Database() # (no output — reused) |
| 21 | print(db1 is db2) # → True |
Registry Pattern
| 1 | class PluginRegistry(type): |
| 2 | """Auto-register subclasses in a central registry.""" |
| 3 | registry = {} |
| 4 | |
| 5 | def __new__(mcs, name, bases, namespace): |
| 6 | cls = super().__new__(mcs, name, bases, namespace) |
| 7 | if bases != (object,): # skip base Plugin class |
| 8 | mcs.registry[name] = cls |
| 9 | return cls |
| 10 | |
| 11 | class Plugin(metaclass=PluginRegistry): |
| 12 | """Base plugin — not registered itself.""" |
| 13 | pass |
| 14 | |
| 15 | class LogPlugin(Plugin): |
| 16 | command = "log" |
| 17 | |
| 18 | class AuthPlugin(Plugin): |
| 19 | command = "auth" |
| 20 | |
| 21 | class CachePlugin(Plugin): |
| 22 | command = "cache" |
| 23 | |
| 24 | print(PluginRegistry.registry) |
| 25 | # → {'LogPlugin': <...>, 'AuthPlugin': <...>, 'CachePlugin': <...>} |
| 26 | |
| 27 | # Look up and invoke by name |
| 28 | for name, plugin_cls in PluginRegistry.registry.items(): |
| 29 | print(f"Loaded: {name} -> {plugin_cls.command}") |
Validation Pattern
| 1 | class ValidateFields(type): |
| 2 | """Ensure all Fields have defaults and validate types.""" |
| 3 | def __new__(mcs, name, bases, namespace): |
| 4 | if name == "Model": |
| 5 | return super().__new__(mcs, name, bases, namespace) |
| 6 | |
| 7 | annotations = namespace.get("__annotations__", {}) |
| 8 | for field_name, field_type in annotations.items(): |
| 9 | if field_name.startswith("_"): |
| 10 | continue |
| 11 | if field_name not in namespace: |
| 12 | raise TypeError( |
| 13 | f"{name}.{field_name} must have a default value" |
| 14 | ) |
| 15 | default = namespace[field_name] |
| 16 | if default is not None and not isinstance(default, field_type): |
| 17 | raise TypeError( |
| 18 | f"{name}.{field_name}: expected {field_type.__name__}, " |
| 19 | f"got {type(default).__name__}" |
| 20 | ) |
| 21 | return super().__new__(mcs, name, bases, namespace) |
| 22 | |
| 23 | class Model(metaclass=ValidateFields): |
| 24 | pass |
| 25 | |
| 26 | class User(Model): |
| 27 | name: str = "" |
| 28 | age: int = 0 |
| 29 | email: str = "" # ok |
| 30 | |
| 31 | class BadUser(Model): |
| 32 | name: str # → TypeError: BadUser.name must have a default |
ORM-like Pattern (Django / SQLAlchemy Style)
| 1 | class Field: |
| 2 | """Descriptor for a model field.""" |
| 3 | def __init__(self, type_): |
| 4 | self.type = type_ |
| 5 | |
| 6 | def __set_name__(self, owner, name): |
| 7 | self.name = name |
| 8 | |
| 9 | class ModelMeta(type): |
| 10 | """Translate declarative fields into a column map.""" |
| 11 | def __new__(mcs, name, bases, namespace): |
| 12 | fields = {} |
| 13 | for key, value in list(namespace.items()): |
| 14 | if isinstance(value, Field): |
| 15 | fields[key] = value |
| 16 | |
| 17 | cls = super().__new__(mcs, name, bases, namespace) |
| 18 | cls._fields = fields |
| 19 | cls._table = name.lower() |
| 20 | return cls |
| 21 | |
| 22 | class Model(metaclass=ModelMeta): |
| 23 | def save(self): |
| 24 | cols = ", ".join(self._fields) |
| 25 | vals = ", ".join( |
| 26 | repr(getattr(self, f)) for f in self._fields |
| 27 | ) |
| 28 | print(f"INSERT INTO {self._table} ({cols}) VALUES ({vals})") |
| 29 | |
| 30 | class User(Model): |
| 31 | name = Field(str) |
| 32 | age = Field(int) |
| 33 | |
| 34 | def __init__(self, name: str, age: int): |
| 35 | self.name = name |
| 36 | self.age = age |
| 37 | |
| 38 | user = User("Alice", 30) |
| 39 | user.save() |
| 40 | # → INSERT INTO user (name, age) VALUES ('Alice', 30) |
The __prepare__ method is a special metaclass hook that returns the namespace object used during class body execution. While the default is a regular dict, you can substitute an OrderedDict or a custom mapping to control attribute ordering or intercept attribute assignments as they happen.
| 1 | class OrderedMeta(type): |
| 2 | @classmethod |
| 3 | def __prepare__(mcs, name, bases, **kwargs): |
| 4 | """Return an OrderedDict to preserve declaration order.""" |
| 5 | from collections import OrderedDict |
| 6 | return OrderedDict() |
| 7 | |
| 8 | def __new__(mcs, name, bases, namespace): |
| 9 | namespace["_order"] = list(namespace.keys()) |
| 10 | return super().__new__(mcs, name, bases, namespace) |
| 11 | |
| 12 | class Task(metaclass=OrderedMeta): |
| 13 | title = "" |
| 14 | priority = 1 |
| 15 | done = False |
| 16 | |
| 17 | print(Task._order) |
| 18 | # → ['__module__', '__qualname__', 'title', 'priority', 'done'] |
| 19 | # (includes dunders added after class body) |
| 1 | # Custom namespace that rejects private attributes |
| 2 | class NoPrivate(type): |
| 3 | @classmethod |
| 4 | def __prepare__(mcs, name, bases, **kwargs): |
| 5 | class NoPrivateDict(dict): |
| 6 | def __setitem__(self, key, value): |
| 7 | if key.startswith("_") and not key.endswith("__"): |
| 8 | raise NameError( |
| 9 | f"Private attribute '{key}' not allowed" |
| 10 | ) |
| 11 | super().__setitem__(key, value) |
| 12 | return NoPrivateDict() |
| 13 | |
| 14 | def __new__(mcs, name, bases, namespace): |
| 15 | return super().__new__(mcs, name, bases, namespace) |
| 16 | |
| 17 | class PublicAPI(metaclass=NoPrivate): |
| 18 | ok = 1 |
| 19 | _secret = 2 # → NameError: Private attribute '_secret' not allowed |
note
When a class inherits from multiple base classes with different metaclasses, Python raises a TypeError. Metaclass conflicts are a sign of poor design and usually indicate that metaclasses should be refactored or replaced with simpler mechanisms.
| 1 | class MetaA(type): |
| 2 | pass |
| 3 | |
| 4 | class MetaB(type): |
| 5 | pass |
| 6 | |
| 7 | class A(metaclass=MetaA): |
| 8 | pass |
| 9 | |
| 10 | class B(metaclass=MetaB): |
| 11 | pass |
| 12 | |
| 13 | # This raises TypeError! |
| 14 | try: |
| 15 | class C(A, B): |
| 16 | pass |
| 17 | except TypeError as e: |
| 18 | print(e) |
| 19 | # → metaclass conflict: the metaclass of a derived class |
| 20 | # must be a (non-strict) subclass of the metaclasses |
| 21 | # of all its bases |
| 22 | |
| 23 | # Fix: create a combined metaclass |
| 24 | class MetaC(MetaA, MetaB): |
| 25 | pass |
| 26 | |
| 27 | class C(A, B, metaclass=MetaC): |
| 28 | pass # ok |
The rule is: a class's metaclass must be a subclass of the metaclasses of all its bases. If MetaA and MetaB are unrelated, you must create a combined metaclass that inherits from both.
warning
Metaclasses are often overused. In most cases, a simpler alternative exists that is easier to understand, debug, and maintain. Consider these before reaching for a metaclass.
| Goal | Better Alternative | Why |
|---|---|---|
| Modify class attributes | Class decorator | Simpler, no inheritance chain issues |
| Add methods to a class | Mixin base class | Standard OOP, no magic |
| Singleton | Module-level instance | Python modules are already singletons |
| Registry of subclasses | __init_subclass__ | Built-in hook, no metaclass needed |
| Auto-repr / auto-init | dataclasses | Standard library, well-tested |
| Validate attributes | Descriptors / properties | Per-field granularity, no class-level magic |
| 1 | # Alternative: __init_subclass__ (Python 3.6+) for registration |
| 2 | class Plugin: |
| 3 | registry = {} |
| 4 | |
| 5 | def __init_subclass__(cls, **kwargs): |
| 6 | super().__init_subclass__(**kwargs) |
| 7 | Plugin.registry[cls.__name__] = cls |
| 8 | |
| 9 | class LogPlugin(Plugin): |
| 10 | pass |
| 11 | |
| 12 | class AuthPlugin(Plugin): |
| 13 | pass |
| 14 | |
| 15 | print(Plugin.registry) |
| 16 | # → {'LogPlugin': <...>, 'AuthPlugin': <...>} |
| 17 | |
| 18 | # Alternative: class decorator for attribute transformation |
| 19 | def add_prefix(prefix: str): |
| 20 | def decorator(cls): |
| 21 | for name in list(cls.__dict__): |
| 22 | if not name.startswith("_"): |
| 23 | setattr(cls, f"{prefix}{name}", getattr(cls, name)) |
| 24 | delattr(cls, name) |
| 25 | return cls |
| 26 | return decorator |
| 27 | |
| 28 | @add_prefix("cfg_") |
| 29 | class Config: |
| 30 | host = "localhost" |
| 31 | port = 8080 |
| 32 | |
| 33 | print(Config.host) # → AttributeError |
| 34 | print(Config.cfg_host) # → 'localhost' |
| 35 | print(Config.cfg_port) # → 8080 |
| 36 | |
| 37 | # Alternative: dataclass for automatic __init__, __repr__, etc. |
| 38 | from dataclasses import dataclass |
| 39 | |
| 40 | @dataclass |
| 41 | class Point: |
| 42 | x: float |
| 43 | y: float |
| 44 | |
| 45 | # Equivalent to ~30 lines of metaclass code |
best practice