Verification: —

When __init_subclass__ Goes Silent — A CPython MRO Edge Case

Python's __init_subclass__ hook silently fails when a metaclass mro() places Superclass before subclass (CPython bug #105038, reported by plokmijnuhby).…

Contents

    The Bottom Line

    Python’s __init_subclass__ is the cleanest way to hook subclass creation without a metaclass — unless a metaclass manipulates the Method Resolution Order (MRO). If the MRO places the defining class before the subclass being created, __init_subclass__ is silently skipped. This is a confirmed CPython bug (#105038) driven by how type.__new__ uses super() internally.


    The Problem: A Hook That Never Fires

    __init_subclass__ was introduced in Python 3.6 as a simpler alternative to metaclasses. When any class inherits from a class that defines __init_subclass__, that method is called automatically during class creation.

    Consider this metaclass that produces a non-standard MRO:

    class Superclass:
        def __init_subclass__(cls):
            print(f"Called __init_subclass__ on {cls.__name__}")
            cls.hook_was_called = True
    
    class Metaclass(type):
        def mro(cls):
            return [Superclass, cls, object]
    
    class WeirdSub(metaclass=Metaclass):
        pass
    
    print(f"issubclass(WeirdSub, Superclass): {issubclass(WeirdSub, Superclass)}")
    print(f"MRO: {[c.__name__ for c in WeirdSub.__mro__]}")
    print(f"hook_was_called: {getattr(WeirdSub, 'hook_was_called', False)}")

    Output:

    issubclass(WeirdSub, Superclass): True
    MRO: ['Superclass', 'WeirdSub', 'object']
    hook_was_called: False

    issubclass() returns True. The MRO contains Superclass. But __init_subclass__ was never invoked. The hook — the entire reason __init_subclass__ exists — is dead silent.


    The Fix: Root Cause in type.__new__

    The bug lives in CPython’s type.__new__ implementation. Here’s the relevant logic:

    # Simplified version of what type.__new__ does internally
    def type__new__(metacls, name, bases, namespace):
        # ... class creation logic ...
    
        # To avoid calling __init_subclass__ on the class being defined,
        # CPython uses super(Subclass, Subclass).__init_subclass__()
        # which skips everything before Subclass in the MRO!
    
        super(WeirdSub, WeirdSub).__init_subclass__()
        # ^^^ This resolves to object.__init_subclass__ — Superclass is skipped!

    The call super(WeirdSub, WeirdSub).__init_subclass__() produces the MRO starting after WeirdSub. Since Metaclass.mro() placed Superclass before WeirdSub, Superclass is excluded from the search. The method never fires.

    As reporter plokmijnuhby explained in CPython #105038:

    “To avoid looking for and potentially calling an __init_subclass__ method defined by Subclass itself, currently type.__new__ instead calls super(Subclass, Subclass).__init_subclass__. This causes it to skip over Superclass in the MRO.”

    “A possible fix would be to avoid calling super and instead read the whole MRO, ignoring Subclass if it finds it; however, this would require some thought about what happens if Superclass.__init_subclass__ itself calls super.”


    Why It Matters

    This isn’t just a curiosity — it has real implications:

    1. Framework design patterns: Libraries like Django, SQLAlchemy, and Pydantic use __init_subclass__ for registration, validation, or configuration hooks. A metaclass that reorders the MRO silently disables those hooks.

    2. Metaclass composition: Projects that mix multiple metaclasses (via __bases__ manipulation or custom mro()) can accidentally break __init_subclass__ without any visible error.

    3. Testing blind spots: issubclass() returns True, the MRO looks correct, and no exception is raised. The only symptom is missing behavior — a classic “it doesn’t work and I don’t know why” bug.

    4. Related bug chain: This same super() pattern in type.__new__ causes issue #83846, which produces a cryptic error when types don’t include themselves in their MRO at all.


    Safe Alternatives

    If you need subclass hooks that always fire, regardless of MRO ordering:

    Metaclass __new__ always runs, and you control the call order explicitly:

    class SafeMetaclass(type):
        def __new__(mcs, name, bases, namespace):
            cls = super().__new__(mcs, name, bases, namespace)
            # Your hook here — always fires
            print(f"Metaclass hook on {cls.__name__}")
            return cls

    Best for: Framework authors who control the class creation chain.

    Option 2: Use __set_name__ on descriptors

    If you’re tracking attribute definitions rather than classes, __set_name__ fires reliably during class creation regardless of MRO quirks:

    class TrackedAttribute:
        def __set_name__(self, owner, name):
            print(f"Attribute {name} defined on {owner.__name__}")
            Registry.register(owner, name)

    Best for: ORM field tracking, serializer registrations, validation descriptors.

    Option 3: Manual MRO iteration (fix for metaclass authors)

    If you control the metaclass and can’t switch away from __init_subclass__:

    class FixedMetaclass(type):
        def mro(cls):
            return [Superclass, cls, object]
    
        def __new__(mcs, name, bases, namespace):
            cls = super().__new__(mcs, name, bases, namespace)
            # Manually scan MRO for __init_subclass__
            for ancestor in cls.__mro__:
                if hasattr(ancestor, '__init_subclass__'):
                    ancestor.__init_subclass__(cls)
            return cls

    Best for: Bug fixes in existing metaclass code where you can’t refactor the entire class hierarchy.


    Decision Guide

    ApproachComplexityReliabilityUse Case
    Metaclass __new__Medium✅ Always firesNew code, framework design
    __set_name__Low✅ Always firesAttribute-level hooks
    Manual MRO iterationHigh✅ Works around bugPatching existing code
    __init_subclass__ aloneLow❌ Silent failureOnly if MRO is never modified

    How to Detect This Bug

    Add this test to any project using __init_subclass__ with metaclasses:

    def test_init_subclass_fires_with_metaclass():
        """Verify __init_subclass__ fires even with custom MRO."""
        hook_called = []
    
        class Base:
            def __init_subclass__(cls):
                hook_called.append(cls.__name__)
    
        class CustomMeta(type):
            def mro(cls):
                # Non-standard MRO that places Base before cls
                return [Base, cls, object]
    
        class Child(metaclass=CustomMeta):
            pass
    
        assert "Child" in hook_called, "__init_subclass__ was silently skipped!"

    Key Takeaways

    1. __init_subclass__ is not guaranteed — it depends on MRO ordering through an internal super() call.
    2. Test with metaclasses — if your code mixes metaclasses and __init_subclass__, verify hooks actually fire.
    3. Prefer metaclass __new__ for framework code that must always run.
    4. Use __set_name__ for attribute-level hooks that need to be MRO-independent.
    5. The bug is open — tracked at CPython #105038 since 2023.

    Rule of thumb: If you’re writing a metaclass that overrides mro(), test that __init_subclass__ hooks still fire. If you’re using a framework that mixes metaclasses and __init_subclass__, verify your subclass hooks are actually running — they may be silently swallowed.


    This post was auto-generated. View all patches on GitHub.

    References