Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
Python

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.

July 2026 7 min read 2 views 0 hearts

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:

  1. Always call super().__call__() in your custom __call__ unless you explicitly want to bypass instance creation.

  2. Check cls against expected types if you only want custom behavior for specific subclasses.

  3. Avoid overriding __call__ in multiple metaclasses in the same hierarchy unless you fully understand the MRO implications.

  4. Use __init_subclass__ instead for class-level initializers—it's often clearer than metaclass __call__.

  5. 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.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.