Introduction
A metaclass has three key methods that form its lifecycle: __new__, __init__, and __call__. Each fires at a different stage of class creation and instantiation. Confusing them is the number one source of metaclass bugs. This lesson clarifies when each method runs and what it controls.
Key Concepts
__new__(mcs, name, bases, namespace): Creates and returns the new class object. Runs once when theclassstatement is executed. This is where you can modify the class before it exists.__init__(cls, name, bases, namespace): Initializes the already-created class object. Runs after__new__. Use this for setup that does not need to change the class structure.__call__(cls, *args, **kwargs): Runs every time you instantiate the class (e.g.,obj = MyClass()). It orchestrates the instance's__new__and__init__.__prepare__(mcs, name, bases): Returns the namespace dict for the class body. Runs before__new__. Useful for ordered or restricted namespaces.
Real World Context
- Django REST Framework serializers use metaclass
__new__to collect field definitions from the class body and store them in a_declared_fieldsattribute before the class is fully created. - Enum uses metaclass
__new__to convert class attributes into enum members and prevent duplicate values. - Singleton pattern is implemented by overriding
__call__on the metaclass to cache and reuse the single instance.
Deep Dive
Here is the full lifecycle with print statements to show the execution order:
pythonclass TracingMeta(type): def __prepare__(mcs, name, bases, **kwargs): print(f"1. __prepare__: creating namespace for {name}") return super().__prepare__(name, bases, **kwargs) def __new__(mcs, name, bases, namespace, **kwargs): print(f"2. __new__: creating class object '{name}'") cls = super().__new__(mcs, name, bases, namespace) return cls def __init__(cls, name, bases, namespace, **kwargs): print(f"3. __init__: initializing class '{name}'") super().__init__(name, bases, namespace) def __call__(cls, *args, **kwargs): print(f"4. __call__: creating instance of '{cls.__name__}'") instance = super().__call__(*args, **kwargs) return instance class MyClass(metaclass=TracingMeta): pass # Output during class definition: # 1. __prepare__: creating namespace for MyClass # 2. __new__: creating class object 'MyClass' # 3. __init__: initializing class 'MyClass' obj = MyClass() # Output during instantiation: # 4. __call__: creating instance of 'MyClass'
Notice that __prepare__, __new__, and __init__ run at class definition time (when the class statement executes). Only __call__ runs at instantiation time (when you call MyClass()).
Here is a practical use of __new__ to validate class definitions:
pythonclass InterfaceMeta(type): """Metaclass that ensures subclasses implement required methods.""" def __new__(mcs, name, bases, namespace): cls = super().__new__(mcs, name, bases, namespace) # Skip the base interface class itself if bases: required = getattr(cls, '_required_methods', []) for method_name in required: if method_name not in namespace: raise TypeError( f"Class '{name}' must implement '{method_name}'" ) return cls class Serializer(metaclass=InterfaceMeta): _required_methods = ['serialize', 'deserialize'] class JSONSerializer(Serializer): def serialize(self, data): import json return json.dumps(data) def deserialize(self, text): import json return json.loads(text) # This would raise TypeError: # class BrokenSerializer(Serializer): # def serialize(self, data): ... # # Missing deserialize!
And here is __call__ for the Singleton pattern:
pythonclass Singleton(type): _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=Singleton): def __init__(self): print("Connecting to database...") db1 = Database() # Connecting to database... db2 = Database() # No output -- returns cached instance print(db1 is db2) # True
Common Pitfalls
- Modifying the class in
__init__instead of__new__: In__init__, the class already exists. If you need to alter the class structure (e.g., add/remove attributes, change bases), do it in__new__where you control what gets created. - Forgetting to call
super(): Always callsuper().__new__(),super().__init__(), orsuper().__call__()-- otherwise the default behavior (like actually creating the class or instance) is skipped entirely. - Confusing metaclass
__call__with class__call__:Metaclass.__call__runs when you call the class (to make an instance).Class.__call__runs when you call an instance. They live at different levels. - Using
__new__when__init__suffices: If you only need to record metadata or register the class (not alter its structure), prefer__init__-- it is simpler.
Best Practices
- Use
__new__to modify the class before it is created (validate methods, alter namespace, inject attributes). - Use
__init__for post-creation setup that does not alter the class structure (registration, logging). - Use
__call__to customize instance creation (caching, pooling, singletons). - Add tracing (like the
TracingMetaexample) when debugging metaclass issues -- it reveals the exact execution order.
Summary
The metaclass lifecycle consists of __prepare__ (namespace creation), __new__ (class object creation), __init__ (class initialization), and __call__ (instance creation). The first three run at class definition time; __call__ runs each time you instantiate the class. Choosing the right method depends on when you need to intervene: before the class exists (__new__), after it exists (__init__), or when instances are created (__call__).
Code Examples
class TracingMeta(type):
def __prepare__(mcs, name, bases, **kwargs):
print(f"1. __prepare__: creating namespace for {name}")
return super().__prepare__(name, bases, **kwargs)
def __new__(mcs, name, bases, namespace, **kwargs):
print(f"2. __new__: creating class object '{name}'")
return super().__new__(mcs, name, bases, namespace)
def __init__(cls, name, bases, namespace, **kwargs):
print(f"3. __init__: initializing class '{name}'")
super().__init__(name, bases, namespace)
def __call__(cls, *args, **kwargs):
print(f"4. __call__: creating instance of '{cls.__name__}'")
return super().__call__(*args, **kwargs)
class MyClass(metaclass=TracingMeta):
pass
# 1. __prepare__: creating namespace for MyClass
# 2. __new__: creating class object 'MyClass'
# 3. __init__: initializing class 'MyClass'
obj = MyClass()
# 4. __call__: creating instance of 'MyClass'class Singleton(type):
_instances = {}
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=Singleton):
def __init__(self):
self.settings = {}
c1 = Config()
c2 = Config()
print(c1 is c2) # True