Python Hybrid Inheritance and the MRO
python hybrid inheritance: Understand how Python resolves hybrid inheritance through C3 linearization, the diamond problem, and cooperative super() chains.
python hybrid inheritance requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
Python's hybrid inheritance combines multiple inheritance patterns, typically mixing a hierarchical structure with multiple base classes at one level. The interpreter resolves method lookup through the Method Resolution Order (MRO), computed by C3 linearization. Understanding that order is the difference between working code and subtle runtime surprises.
What Hybrid Inheritance Means in Python
Hybrid inheritance is not a separate language feature. It is a class design where a subclass inherits from more than one parent, and those parents share a common ancestor. The classic shape is a diamond:
A
/ \
B C
\ /
D
Python allows this directly. The interpreter builds a linearized list of classes, the MRO, that determines which method runs when you call d.method().
How Python Computes the Method Resolution Order
Python uses C3 linearization. The MRO is a total ordering of the class hierarchy that respects two rules: each class appears before its parents, and the order of bases in a class definition is preserved. Consider:
class Base: def describe(self): return "Base" class Left(Base): def describe(self): return "Left -> " + super().describe() class Right(Base): def describe(self): return "Right -> " + super().describe() class Hybrid(Left, Right): pass print(Hybrid.mro())
The output shows Hybrid -> Left -> Right -> Base -> object. The interpreter visits Left before Right because Left is listed first in the class definition. super() calls inside Left and Right continue along this same MRO, not to the immediate parent in the source code.
The Diamond Problem in Hybrid Inheritance
The diamond problem is the ambiguity that arises when two parents share a common ancestor. Without a linearization rule, a call to a method defined in Base could resolve through either B or C. C3 resolves this deterministically:
class A: def method(self): return "A" class B(A): def method(self): return "B -> " + super().method() class C(A): def method(self): return "C -> " + super().method() class D(B, C): pass print(D.mro()) print(D().method())
The MRO is D -> B -> C -> A -> object, and the output is B -> C -> A. Each super() call delegates to the next class in the MRO, so B calls C.method(), and C calls A.method(). The order of bases in D determines which branch runs first.
Using super() for Cooperative Inheritance
For hybrid inheritance to behave predictably, every class in the chain must call super() cooperatively. If one class skips the call, the chain breaks and later classes never run. This is a design contract, not an implementation detail:
class AuditMixin: def process(self): print("audit") return super().process() class MetricsMixin: def process(self): print("metrics") return super().process() class Worker(AuditMixin, MetricsMixin): def process(self): print("work") return super().process() Worker().process()
The output is audit, metrics, work in that order, following the MRO Worker -> AuditMixin -> MetricsMixin -> object. If AuditMixin.process did not call super().process(), MetricsMixin would never execute.
A Practical Hybrid Inheritance Example
A common use is combining a domain base class with orthogonal mixins that add cross-cutting behavior. Consider a service layer where one mixin handles retries and another handles logging:
class ServiceBase: def execute(self, task): return f"executed {task}" class RetryMixin: def execute(self, task): for attempt in range(3): try: return super().execute(task) except ConnectionError: continue raise ConnectionError("failed after retries") class LogMixin: def execute(self, task): result = super().execute(task) print(f"task {task} -> {result}") return result class OrderService(RetryMixin, LogMixin, ServiceBase): pass service = OrderService() print(service.execute("create_order"))
The MRO is OrderService -> RetryMixin -> LogMixin -> ServiceBase -> object. The retry wrapper sits outside the log wrapper, so each retry attempt is logged individually. Reordering the bases changes the wrapping order, which changes observable behavior.
Common Failure Modes in Hybrid Inheritance
The most frequent failure is an inconsistent super() chain. If one mixin calls a method that does not exist on the next class in the MRO, you get an AttributeError at runtime. Another common issue is signature mismatch: super().method() must be callable with the same arguments across the entire chain. A class that accepts extra parameters breaks the cooperative contract.
A subtler failure is relying on the source-level parent rather than the MRO. Reading class D(B, C) suggests B is the parent of D, but in the MRO, C also participates. Code that casts or assumes a single parent chain misunderstands the actual dispatch.
Maintainability and When to Avoid Hybrid Inheritance
Hybrid inheritance is useful when mixins are truly orthogonal and each one follows the cooperative super() contract. It becomes a maintenance burden when the hierarchy grows beyond a few levels, because the MRO becomes hard to reason about. Prefer composition, passing collaborating objects into a class, when the cross-cutting behavior does not need to intercept method calls in a chain.
A practical rule: use hybrid inheritance when you need ordered interception of a method call across multiple mixins, and the order is stable. Use composition when the behavior is independent and order does not matter.