When __init_subclass__ Goes Silent — A CPython MRO Edge Case

The article provides simplified type.__new__ code showing super(Subclass, Subclass).__init_subclass__() with comment explaining the skip. A FixedMetaclass example manually iterates MRO to call ancestor.__init_subclass__(cls). Three safe alternatives are listed: use a metaclass directly, use __set_name__ on descriptors, or manually scan MRO. The bug is CPython #105038, open since 2023, with related #83846. Rule: test hook firing if metaclass overrides mro. Frameworks like Django, SQLAlchemy, P...

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

Key Takeaway

__init_subclass__ is Python’s most approachable class-creation hook — but its invocation is entirely dependent on MRO ordering through an internal super() call in type.__new__. Any metaclass that manipulates the MRO can silently disable it.

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 bug is tracked at CPython #105038 and has been open since 2023. The fix requires careful thought about edge cases within the edge case — a reminder that even Python’s well-designed class system has subtle depth.