|$ curl https://forge-ai.dev/api/markdown?path=docs/python/metaclasses
$cat docs/python-—-metaclasses.md
updated Recently·18 min read·published

Python — Metaclasses

PythonAdvanced
Introduction

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

Metaclasses are powerful but come with cognitive cost. Before reaching for a metaclass, ask whether a class decorator, a base class, or a factory function would suffice. Python's Zen says "Simple is better than complex" — metaclasses are the nuclear option.
type() as a Metaclass

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.

type_metaclass.py
Python
1# type(obj) — get the type
2print(type(42)) # → <class 'int'>
3print(type("hello")) # → <class 'str'>
4print(type(int)) # → <class 'type'>
5
6# type(name, bases, namespace) — create a class dynamically
7MyClass = type("MyClass", (object,), {"x": 10, "greet": lambda self: "hi"})
equivalent.py
Python
1# These two are equivalent:
2
3# 1. class statement
4class Foo:
5 bar = 1
6
7 def __init__(self):
8 self.value = 42
9
10# 2. type() call
11def foo_init(self):
12 self.value = 42
13
14Foo = 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.

Custom Metaclasses

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.

custom_meta.py
Python
1# A metaclass must inherit from type
2class 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
10class MyClass(metaclass=UppercaseAttributes):
11 foo = 1
12 bar = 2
13
14print(hasattr(MyClass, "foo")) # → False
15print(hasattr(MyClass, "FOO")) # → True
16print(hasattr(MyClass, "bar")) # → False
17print(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.

validate.py
Python
1# Inheritance works naturally with metaclasses
2class 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
17class BaseModel(metaclass=ValidateFields):
18 pass
19
20class User(BaseModel):
21 name: str = ... # error — '...' is not a real default
22 age: int = 0 # ok
__new__ vs __init__ in Metaclasses

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.

HookWhen It RunsPurposeMust Return
__new__FirstCreate and return the class objectThe class (or modified class)
__init__After __new__Initialize the class object in-placeNothing (None)
new_vs_init.py
Python
1class 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
15class Demo(metaclass=TraceMeta):
16 pass
17# Output:
18# [__new__] Creating class Demo
19# [__init__] Initializing class Demo
20
21print(Demo._created) # → True
22print(Demo._initialized) # → True
23
24# __new__ can replace the class entirely:
25class 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

Use __new__ when you need to modify or replace the namespace before the class is created. Use __init__ when you need to run post-creation setup (like registering the class in a registry). __new__ is far more common in practice.
Common Metaclass Use Cases

While rare, metaclasses shine in a few specific patterns. Here are the most common real-world applications.

Singleton Pattern

singleton.py
Python
1class 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
10class 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
19db1 = Database() # → Connecting to database...
20db2 = Database() # (no output — reused)
21print(db1 is db2) # → True

Registry Pattern

registry.py
Python
1class 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
11class Plugin(metaclass=PluginRegistry):
12 """Base plugin — not registered itself."""
13 pass
14
15class LogPlugin(Plugin):
16 command = "log"
17
18class AuthPlugin(Plugin):
19 command = "auth"
20
21class CachePlugin(Plugin):
22 command = "cache"
23
24print(PluginRegistry.registry)
25# → {'LogPlugin': <...>, 'AuthPlugin': <...>, 'CachePlugin': <...>}
26
27# Look up and invoke by name
28for name, plugin_cls in PluginRegistry.registry.items():
29 print(f"Loaded: {name} -> {plugin_cls.command}")

Validation Pattern

validation.py
Python
1class 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
23class Model(metaclass=ValidateFields):
24 pass
25
26class User(Model):
27 name: str = ""
28 age: int = 0
29 email: str = "" # ok
30
31class BadUser(Model):
32 name: str # → TypeError: BadUser.name must have a default

ORM-like Pattern (Django / SQLAlchemy Style)

orm.py
Python
1class 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
9class 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
22class 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
30class 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
38user = User("Alice", 30)
39user.save()
40# → INSERT INTO user (name, age) VALUES ('Alice', 30)
__prepare__

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.

prepare.py
Python
1class 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
12class Task(metaclass=OrderedMeta):
13 title = ""
14 priority = 1
15 done = False
16
17print(Task._order)
18# → ['__module__', '__qualname__', 'title', 'priority', 'done']
19# (includes dunders added after class body)
no_private.py
Python
1# Custom namespace that rejects private attributes
2class 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
17class PublicAPI(metaclass=NoPrivate):
18 ok = 1
19 _secret = 2 # → NameError: Private attribute '_secret' not allowed
📝

note

__prepare__ is rarely needed. Its main value is preserving declaration order (which Python 3.7+ does by default in regular dicts) and intercepting attribute assignments during class body execution for validation or transformation.
Metaclass Conflicts

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.

conflicts.py
Python
1class MetaA(type):
2 pass
3
4class MetaB(type):
5 pass
6
7class A(metaclass=MetaA):
8 pass
9
10class B(metaclass=MetaB):
11 pass
12
13# This raises TypeError!
14try:
15 class C(A, B):
16 pass
17except 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
24class MetaC(MetaA, MetaB):
25 pass
26
27class 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

Metaclass conflicts are a code smell. If you find yourself writing combined metaclasses, reconsider the architecture. Multiple inheritance with metaclasses creates fragile, hard-to-debug hierarchies. Prefer composition over inheritance when metaclasses are involved.
Alternatives — When NOT to Use Metaclasses

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.

GoalBetter AlternativeWhy
Modify class attributesClass decoratorSimpler, no inheritance chain issues
Add methods to a classMixin base classStandard OOP, no magic
SingletonModule-level instancePython modules are already singletons
Registry of subclasses__init_subclass__Built-in hook, no metaclass needed
Auto-repr / auto-initdataclassesStandard library, well-tested
Validate attributesDescriptors / propertiesPer-field granularity, no class-level magic
alternatives.py
Python
1# Alternative: __init_subclass__ (Python 3.6+) for registration
2class Plugin:
3 registry = {}
4
5 def __init_subclass__(cls, **kwargs):
6 super().__init_subclass__(**kwargs)
7 Plugin.registry[cls.__name__] = cls
8
9class LogPlugin(Plugin):
10 pass
11
12class AuthPlugin(Plugin):
13 pass
14
15print(Plugin.registry)
16# → {'LogPlugin': <...>, 'AuthPlugin': <...>}
17
18# Alternative: class decorator for attribute transformation
19def 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_")
29class Config:
30 host = "localhost"
31 port = 8080
32
33print(Config.host) # → AttributeError
34print(Config.cfg_host) # → 'localhost'
35print(Config.cfg_port) # → 8080
36
37# Alternative: dataclass for automatic __init__, __repr__, etc.
38from dataclasses import dataclass
39
40@dataclass
41class Point:
42 x: float
43 y: float
44
45# Equivalent to ~30 lines of metaclass code

best practice

A good rule of thumb: if you can solve the problem with a class decorator, __init_subclass__, __set_name__, or a base class, do that instead. Metaclasses should be reserved for when you need to intercept the class-creation process itself — not for modifying classes after creation.
$Blueprint — Engineering Documentation·Section ID: PYTHON-META·Revision: 1.0