Metaclasses

0/5 in this phase0/54 across the roadmap

📖 Concept

In Python, everything is an object — including classes themselves. A metaclass is the "class of a class." Just as a class defines how instances behave, a metaclass defines how classes behave. By default, all classes are instances of type.

The relationship:

class Foo:
    pass

type(Foo)       # <class 'type'>  — Foo is an instance of type
type(type)      # <class 'type'>  — type is its own metaclass
isinstance(Foo, type)  # True

type() as a class factory: type(name, bases, namespace) dynamically creates classes at runtime:

# These are equivalent:
class Dog:
    sound = "woof"
Dog = type("Dog", (), {"sound": "woof"})

__new__ vs __init__ in metaclasses:

Method Called when Receives Purpose
__new__(mcs, name, bases, namespace) Class is created Class name, base classes, namespace dict Control class creation, modify namespace
__init__(cls, name, bases, namespace) Class is initialized Same args, but class already exists Post-creation setup
__call__(cls, *args, **kwargs) Class is instantiated Instance creation args Control instance creation

When to use metaclasses:

  • Registering classes automatically (plugin systems, ORMs)
  • Enforcing coding contracts (all subclasses must define certain methods)
  • Modifying or wrapping methods at class creation time
  • Implementing the descriptor protocol for frameworks

ABCMeta from the abc module is Python's built-in metaclass for abstract base classes. It tracks @abstractmethod decorators and prevents instantiation of classes that don't implement all abstract methods. You can combine custom metaclass logic with ABCMeta by inheriting from it.

The metaclass search order: Python checks for metaclass= in the class definition, then checks the first base class's metaclass, then defaults to type. If conflicting metaclasses are found, the most derived one must be a subclass of all others, or TypeError is raised.

Most Python developers rarely need custom metaclasses — class decorators, __init_subclass__, and descriptors cover most use cases with less complexity. Use metaclasses only when you need to intervene in class creation itself.

💻 Code Example

codeTap to expand ⛶
1# ============================================================
2# Understanding type() as a class factory
3# ============================================================
4# Creating a class dynamically with type()
5def greet(self):
6 return f"Hello, I'm {self.name}"
7
8Person = type("Person", (), {
9 "species": "Homo sapiens",
10 "__init__": lambda self, name: setattr(self, "name", name),
11 "greet": greet,
12 "__repr__": lambda self: f"Person({self.name!r})",
13})
14
15p = Person("Alice")
16print(p.greet()) # Hello, I'm Alice
17print(type(Person)) # <class 'type'>
18
19
20# ============================================================
21# Basic metaclass with __new__
22# ============================================================
23class RegistryMeta(type):
24 """Metaclass that auto-registers all classes in a registry."""
25
26 _registry: dict[str, type] = {}
27
28 def __new__(mcs, name, bases, namespace):
29 cls = super().__new__(mcs, name, bases, namespace)
30 # Don't register the base class itself
31 if bases:
32 mcs._registry[name] = cls
33 return cls
34
35 @classmethod
36 def get_registry(mcs) -> dict[str, type]:
37 return dict(mcs._registry)
38
39
40class Plugin(metaclass=RegistryMeta):
41 """Base class for all plugins."""
42 def execute(self):
43 raise NotImplementedError
44
45
46class AuthPlugin(Plugin):
47 def execute(self):
48 return "Authenticating..."
49
50
51class CachePlugin(Plugin):
52 def execute(self):
53 return "Caching..."
54
55
56class LogPlugin(Plugin):
57 def execute(self):
58 return "Logging..."
59
60
61# All subclasses are automatically registered
62print(RegistryMeta.get_registry())
63# {'AuthPlugin': <class 'AuthPlugin'>, 'CachePlugin': ..., 'LogPlugin': ...}
64
65# Instantiate by name (useful for config-driven architectures)
66plugin_name = "AuthPlugin"
67plugin_cls = RegistryMeta.get_registry()[plugin_name]
68plugin = plugin_cls()
69print(plugin.execute()) # "Authenticating..."
70
71
72# ============================================================
73# Metaclass with __init__ for validation
74# ============================================================
75class ValidatedMeta(type):
76 """Metaclass that enforces coding contracts on classes."""
77
78 def __init__(cls, name, bases, namespace):
79 super().__init__(name, bases, namespace)
80
81 # Skip validation for the base class
82 if not bases:
83 return
84
85 # Enforce: all concrete classes must have a docstring
86 if not cls.__doc__:
87 raise TypeError(f"Class {name} must have a docstring")
88
89 # Enforce: all concrete classes must define 'validate' method
90 if "validate" not in namespace and not any(
91 hasattr(base, "validate") for base in bases
92 ):
93 raise TypeError(f"Class {name} must implement validate()")
94
95
96class Model(metaclass=ValidatedMeta):
97 """Base model class."""
98 def validate(self):
99 pass
100
101
102class UserModel(Model):
103 """User data model with validation."""
104
105 def __init__(self, name: str, email: str):
106 self.name = name
107 self.email = email
108
109 def validate(self):
110 if not self.name:
111 raise ValueError("Name is required")
112 if "@" not in self.email:
113 raise ValueError("Invalid email")
114 return True
115
116
117# This would raise TypeError:
118# class BadModel(Model):
119# pass # No docstring! -> TypeError
120
121
122# ============================================================
123# Metaclass __call__ — controlling instance creation
124# ============================================================
125class SingletonMeta(type):
126 """Metaclass that makes classes singletons."""
127
128 _instances: dict[type, object] = {}
129
130 def __call__(cls, *args, **kwargs):
131 if cls not in cls._instances:
132 # Create the instance using type.__call__
133 instance = super().__call__(*args, **kwargs)
134 cls._instances[cls] = instance
135 return cls._instances[cls]
136
137
138class AppConfig(metaclass=SingletonMeta):
139 def __init__(self, env: str = "production"):
140 self.env = env
141 self.settings = {}
142
143 def set(self, key: str, value) -> None:
144 self.settings[key] = value
145
146 def get(self, key: str, default=None):
147 return self.settings.get(key, default)
148
149
150config1 = AppConfig("production")
151config2 = AppConfig("staging") # Ignored! Returns existing instance
152print(config1 is config2) # True
153print(config2.env) # "production" (not "staging")
154
155
156# ============================================================
157# __init_subclass__ — the simpler alternative to metaclasses
158# ============================================================
159class Serializable:
160 """Base class that tracks all serializable subclasses."""
161
162 _serializers: dict[str, type] = {}
163
164 def __init_subclass__(cls, format_name: str = None, **kwargs):
165 super().__init_subclass__(**kwargs)
166 if format_name:
167 cls._format = format_name
168 Serializable._serializers[format_name] = cls
169
170 @classmethod
171 def get_serializer(cls, format_name: str):
172 return cls._serializers.get(format_name)
173
174
175class JSONSerializer(Serializable, format_name="json"):
176 def serialize(self, data) -> str:
177 import json
178 return json.dumps(data)
179
180
181class CSVSerializer(Serializable, format_name="csv"):
182 def serialize(self, data) -> str:
183 return ",".join(str(v) for v in data)
184
185
186print(Serializable._serializers)
187# {'json': <class 'JSONSerializer'>, 'csv': <class 'CSVSerializer'>}
188
189serializer = Serializable.get_serializer("json")()
190print(serializer.serialize({"key": "value"})) # '{"key": "value"}'
191
192
193# ============================================================
194# ABCMeta — abstract base classes
195# ============================================================
196from abc import ABCMeta, abstractmethod, ABC
197
198
199class Repository(ABC):
200 """Abstract repository interface."""
201
202 @abstractmethod
203 def find_by_id(self, id: str):
204 """Retrieve an entity by ID."""
205 ...
206
207 @abstractmethod
208 def save(self, entity) -> None:
209 """Persist an entity."""
210 ...
211
212 @abstractmethod
213 def delete(self, id: str) -> bool:
214 """Delete an entity by ID."""
215 ...
216
217 def find_all(self) -> list:
218 """Default implementation — subclasses may override."""
219 return []
220
221
222class InMemoryUserRepo(Repository):
223 """Concrete implementation using in-memory storage."""
224
225 def __init__(self):
226 self._store: dict[str, dict] = {}
227
228 def find_by_id(self, id: str):
229 return self._store.get(id)
230
231 def save(self, entity) -> None:
232 self._store[entity["id"]] = entity
233
234 def delete(self, id: str) -> bool:
235 return self._store.pop(id, None) is not None
236
237
238# Repository() # TypeError: Can't instantiate abstract class
239repo = InMemoryUserRepo()
240repo.save({"id": "1", "name": "Alice"})
241print(repo.find_by_id("1")) # {'id': '1', 'name': 'Alice'}
242
243
244# ============================================================
245# Combining metaclass features with __prepare__
246# ============================================================
247from collections import OrderedDict
248
249
250class OrderedMeta(type):
251 """Metaclass that preserves attribute definition order."""
252
253 @classmethod
254 def __prepare__(mcs, name, bases):
255 # Return an OrderedDict so attribute order is tracked
256 return OrderedDict()
257
258 def __new__(mcs, name, bases, namespace):
259 cls = super().__new__(mcs, name, bases, dict(namespace))
260 cls._field_order = [
261 key for key in namespace
262 if not key.startswith("_") and not callable(namespace[key])
263 ]
264 return cls
265
266
267class FormFields(metaclass=OrderedMeta):
268 username = "text"
269 email = "email"
270 password = "password"
271 confirm = "password"
272
273print(FormFields._field_order)
274# ['username', 'email', 'password', 'confirm']

🏋️ Practice Exercise

Exercises:

  1. Create a RegistryMeta metaclass that maintains a dictionary of all classes created with it. Add a class method create(name, **kwargs) to the base class that looks up the registry and instantiates the correct subclass by name.

  2. Build a ValidatedModelMeta metaclass that inspects type annotations in class bodies and automatically generates __init__ and validate methods. For example, name: str should ensure name is a string in validate().

  3. Implement the Singleton pattern using: (a) a metaclass, (b) a class decorator, and (c) __new__. Compare the three approaches in terms of inheritance behavior, thread safety, and debuggability.

  4. Rewrite the RegistryMeta example using __init_subclass__ instead of a metaclass. Discuss when __init_subclass__ is sufficient and when you genuinely need a metaclass.

  5. Create an abstract base class Shape with ABCMeta that requires area() and perimeter() methods. Add a concrete method description() that uses both. Implement Circle, Rectangle, and Triangle subclasses and verify that incomplete implementations raise TypeError.

  6. Write a metaclass that automatically wraps all methods of a class with a timing decorator, but skips dunder methods. Test it with a class that has 5+ methods.

⚠️ Common Mistakes

  • Using metaclasses when simpler alternatives exist. __init_subclass__, class decorators, and descriptors solve most class-customization problems without metaclass complexity. Reach for metaclasses only when you need to control the class creation process itself (e.g., modifying __prepare__, intercepting __new__ before the class object exists).

  • Confusing __new__ and __init__ in metaclasses. In a metaclass, __new__ creates the class object (not an instance), and __init__ initializes it after creation. In regular classes, __new__ creates the instance. The mcs/cls parameter naming reflects this: mcs = metaclass, cls = the class being created.

  • Metaclass conflicts when using multiple inheritance. If Parent1 uses MetaA and Parent2 uses MetaB, class Child(Parent1, Parent2) raises TypeError unless one meta is a subclass of the other. Fix by creating a combined metaclass: class CombinedMeta(MetaA, MetaB): pass.

  • Forgetting that __init_subclass__ runs at class creation time, not at instantiation time. Side effects in __init_subclass__ (like registering plugins, modifying class attributes) happen when Python reads the class definition, which can cause issues with import order and circular dependencies.

  • Not calling super().__new__() or super().__init__() in metaclass methods. Skipping the super call means type's default behavior is bypassed, potentially creating malformed class objects that fail in subtle ways.

💼 Interview Questions

🎤 Mock Interview

Practice a live interview for Metaclasses