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 bySubclassitself, currentlytype.__new__instead callssuper(Subclass, Subclass).__init_subclass__. This causes it to skip overSuperclassin the MRO.”
“A possible fix would be to avoid calling
superand instead read the whole MRO, ignoringSubclassif it finds it; however, this would require some thought about what happens ifSuperclass.__init_subclass__itself callssuper.”
Why It Matters
This isn’t just a curiosity — it has real implications:
-
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. -
Metaclass composition: Projects that mix multiple metaclasses (via
__bases__manipulation or custommro()) can accidentally break__init_subclass__without any visible error. -
Testing blind spots:
issubclass()returnsTrue, 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. -
Related bug chain: This same
super()pattern intype.__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:
Option 1: Use a metaclass directly (recommended)
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
| Approach | Complexity | Reliability | Use Case |
|---|---|---|---|
Metaclass __new__ | Medium | ✅ Always fires | New code, framework design |
__set_name__ | Low | ✅ Always fires | Attribute-level hooks |
| Manual MRO iteration | High | ✅ Works around bug | Patching existing code |
__init_subclass__ alone | Low | ❌ Silent failure | Only 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.