Python's Metaclass __call__ Gotcha Explained
Learn the hidden pitfalls of Python's metaclass __call__ method, including inheritance overrides, singleton traps, and super() resolution nightmares. Practical tips help you avoid silent bugs in production.
The Hidden Pitfall: When Python's Metaclass __call__ Bites Back
You write a neat metaclass with a custom __call__ method, expecting it to run every time you create an instance. It works perfectly on the first test. Then, two weeks later, in production, your carefully crafted initialization logic gets silently skipped. You're left scratching your head, wondering what went wrong.
This is the reality of Python's metaclass __call__ gotcha. It's one of those features that looks straightforward on paper but hides subtle traps that can turn your elegant design into a debugging nightmare.
Let's break down what's really happening under the hood.
How __call__ Works in Metaclasses
When you define a class in Python, the metaclass's __call__ method gets invoked every time you create an instance of that class. This is different from __new__ and __init__ of the class itself. __call__ controls the entire instantiation process.
Here's a simple example:
class MyMeta(type):
def __call__(cls, *args, **kwargs):
print(f"Creating instance of {cls.__name__}")
instance = super().__call__(*args, **kwargs)
# Custom logic here
return instance
class MyClass(metaclass=MyMeta):
pass
obj = MyClass() # Prints: "Creating instance of MyClass"
Looks harmless, right? The problems start when you realize that __call__ doesn't just run for your instances—it runs for every instance of every class that uses this metaclass.
The Inheritance Gotcha
This is where things get tricky. If you have a class hierarchy, __call__ from the parent's metaclass gets called for child classes too. But here's the catch: if the child class has its own metaclass that overrides __call__, the parent's __call__ never runs.
class ParentMeta(type):
def __call__(cls, *args, **kwargs):
print(f"ParentMeta: Creating {cls.__name__}")
return super().__call__(*args, **kwargs)
class ChildMeta(type):
def __call__(cls, *args, **kwargs):
print(f"ChildMeta: Creating {cls.__name__}")
return super().__call__(*args, **kwargs)
class Parent(metaclass=ParentMeta):
pass
class Child(Parent, metaclass=ChildMeta):
pass
obj = Child()
# Output: "ChildMeta: Creating Child"
# ParentMeta's __call__ never runs!
This silent override can break assumptions in frameworks like PythonSkillset's ORM tools, where metaclass hooks manage instance lifecycle.
The Singleton Trap
Many developers use __call__ to implement singletons. But here's a nightmare scenario: you forget to handle inheritance, and suddenly all child classes share the same singleton state.
class SingletonMeta(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=SingletonMeta):
pass
class TestDatabase(Database):
pass
# These point to different instances because SingletonMeta
# checks the specific class, not the parent
db1 = Database()
db2 = TestDatabase()
print(db1 is db2) # False!
The gotcha here is subtle: SingletonMeta._instances uses cls as the key, so each subclass gets its own cache entry. You'd expect all Database subclasses to share one instance, but the metaclass __call__ treats each class separately.
The super() Resolution Nightmare
When you override __call__ in a metaclass and use super().__call__(), the method resolution order (MRO) can cause unexpected behavior if you have multiple metaclasses in the hierarchy.
class MetaA(type):
def __call__(cls, *args, **kwargs):
print("MetaA")
return super().__call__(*args, **kwargs)
class MetaB(type):
def __call__(cls, *args, **kwargs):
print("MetaB")
return super().__call__(*args, **kwargs)
class CombinedMeta(MetaA, MetaB):
pass
class MyClass(metaclass=CombinedMeta):
pass
obj = MyClass()
# Output: "MetaA" then "MetaB"
# Order depends on MRO of CombinedMeta
This becomes a debugging nightmare when you have inherited metaclasses from different libraries. PythonSkillset developers have encountered this with complex ORM and validation metaclasses colliding.
The __new__ vs __call__ Confusion
Here's a common mistake: thinking that overriding __call__ replaces __new__ and __init__ entirely. It doesn't. The __call__ method controls the invocation but still delegates to __new__ and __init__ internally.
class ConfusingMeta(type):
def __call__(cls, *args, **kwargs):
# Overriding but forgetting to call super().__call__
print("Custom call")
# Missing: instance = super().__call__(*args, **kwargs)
return None # Oops!
class MyClass(metaclass=ConfusingMeta):
def __new__(cls, *args, **kwargs):
print("In __new__")
return super().__new__(cls)
def __init__(self):
print("In __init__")
self.value = 42
obj = MyClass()
# Output: "Custom call"
# __new__ and __init__ never execute!
print(obj) # None
The gotcha: if your __call__ doesn't call super().__call__(), the class's __new__ and __init__ are completely bypassed. This can leave instances uninitialized or return wrong types.
Practical Mitigation
When working with metaclass __call__, keep these rules in mind:
-
Always call
super().__call__()in your custom__call__unless you explicitly want to bypass instance creation. -
Check
clsagainst expected types if you only want custom behavior for specific subclasses. -
Avoid overriding
__call__in multiple metaclasses in the same hierarchy unless you fully understand the MRO implications. -
Use
__init_subclass__instead for class-level initializers—it's often clearer than metaclass__call__. -
Document explicitly that your metaclass uses
__call__, as its effects are non-obvious to future maintainers.
The Bottom Line
Metaclass __call__ is a powerful tool but it's one of those Python features where "with great power comes great debugging sessions." The gotchas aren't in the mechanics—they're in the interactions between inheritance, multiple metaclasses, and the silent bypass of normal instantiation processes.
Before you reach for __call__, ask yourself: do you really need to control instance creation at the metaclass level? Often, __new__ or __init_subclass__ can achieve similar results without the same pitfalls. But when you do need __call__, understanding these common traps will save you from the kind of production bugs that make you question every line you've ever written.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.