Back to Blog
Python

Python Diamond Inheritance: MRO and super()

python diamond inheritance: Understand how Python resolves method calls in diamond inheritance hierarchies using MRO and cooperative super() calls, with practical exam...

multiple inheritancemethod resolution ordersuper()C3 linearizationmixinsclass design
Diagram of a diamond-shaped class hierarchy in Python showing method resolution order.

Python diamond inheritance occurs when a class inherits from two classes that share a common ancestor, creating a diamond shape in the class hierarchy. For example, consider a Base class with two subclasses Left and Right, and a Derived class that inherits from both:

class Base: def method(self): print("Base.method") class Left(Base): def method(self): print("Left.method") class Right(Base): def method(self): print("Right.method") class Derived(Left, Right): pass

When you call Derived().method(), Python must decide which method implementation to use. The answer is not obvious from the class definition alone. This decision is governed by the method resolution order (MRO), which Python computes using the C3 linearization algorithm.

The Diamond Pattern in Python

The diamond pattern is a direct consequence of multiple inheritance. It appears whenever a class inherits from two or more classes that eventually share a common base. In the example above, Derived inherits from Left and Right, and both inherit from Base. The class graph forms a diamond: Derived at the bottom, Left and Right in the middle, and Base at the top.

This pattern is common when you use mixins. A mixin is a class that provides a specific behavior but is not meant to stand alone. For instance, you might have a JSONMixin that adds serialization methods and a LoggingMixin that adds logging. A concrete class can inherit from both, and if both mixins inherit from a common object base, the diamond is implicit.

The diamond itself is not a problem. The problem is determining which method from the hierarchy should be called, especially when intermediate classes override the same method. Python's MRO provides a deterministic answer.

How Python Computes the Method Resolution Order

Python's MRO is based on the C3 linearization algorithm, which produces a linear order of all classes in the hierarchy. This order respects two rules: each class appears before its parents, and the order of bases in the class definition is preserved as much as possible.

For the Derived class above, you can inspect the MRO directly:

print(Derived.__mro__)

The output will be something like:

(<class '__main__.Derived'>, <class '__main__.Left'>, <class '__main__.Right'>, <class '__main__.Base'>, <class 'object'>)

Method lookup follows this order. When you call Derived().method(), Python searches for method in Derived, then Left, then Right, then Base. Because Left defines method, it is used. If Left did not define it, Python would check Right, then Base.

The C3 algorithm guarantees that the MRO is consistent and monotonic. If a class appears in the MRO of a parent, it appears in the same relative order in the child's MRO. This property prevents unexpected method resolution in complex hierarchies.

Cooperative super() Calls in a Diamond

When you use super() in a diamond hierarchy, you need to understand that super() does not simply call the parent class. It delegates to the next class in the MRO of the instance. This is often called cooperative multiple inheritance.

Consider an example where each class initializes a field:

class Base: def __init__(self): print("Base.__init__") self.value = 0 class Left(Base): def __init__(self): print("Left.__init__") super().__init__() self.left = 1 class Right(Base): def __init__(self): print("Right.__init__") super().__init__() self.right = 2 class Derived(Left, Right): def __init__(self): print("Derived.__init__") super().__init__()

When you create Derived(), the super().__init__() call in Derived invokes Left.__init__, because Left is next in the MRO. Then Left.__init__ calls super().__init__(), which invokes Right.__init__, not Base.__init__. Only after Right.__init__ calls super().__init__() does Base.__init__ run. The output is:

Derived.__init__
Left.__init__
Right.__init__
Base.__init__

This cooperative behavior ensures that every class in the MRO gets a chance to initialize its own attributes, and the base class is initialized only once. Without super() calls in the intermediate classes, the chain would break.

Common Mistakes with Diamond Hierarchies

The most frequent mistake is omitting super() in one of the intermediate classes. If Left.__init__ did not call super().__init__(), then Right.__init__ and Base.__init__ would never execute. The resulting object would lack right and value attributes, leading to AttributeError later.

Another mistake is calling a parent class directly, like Base.__init__(self), instead of using super(). This bypasses the MRO and can cause the base initializer to run multiple times. In a diamond, direct calls often produce duplicate initialization and inconsistent state.

A third issue is assuming that super() always refers to the immediate parent. In a diamond, super() in Left refers to Right, not Base. This surprises developers who expect a simple parent chain. The MRO defines the true order.

When to Use Diamond Inheritance

Diamond inheritance is not inherently bad, but it adds complexity. It is most useful when you are building a mixin-based design. Mixins are small, focused classes that provide a single capability, and a concrete class can combine several mixins. If the mixins are designed to cooperate via super(), the diamond pattern works cleanly.

For example, a class that needs both serialization and logging can inherit from JSONMixin and LoggingMixin. Both mixins might inherit from object, forming an implicit diamond. As long as each mixin calls super() in its methods, the combined behavior is predictable.

However, if the hierarchy is deep or the classes are tightly coupled, composition is often simpler. Instead of inheriting from two classes, you can hold instances of them as attributes and delegate explicitly. This avoids MRO complexity and makes dependencies visible. Use diamond inheritance when the mixins are truly orthogonal and the cooperative pattern is maintained. Otherwise, prefer composition.

Inspecting MRO for Debugging

When a diamond hierarchy behaves unexpectedly, the first step is to inspect the MRO. You can use __mro__ directly, or the inspect.getmro function for a more explicit API:

import inspect for cls in inspect.getmro(Derived): print(cls.__name__)

This prints the class names in the order Python will search. If the order is not what you expect, the class definition is likely missing a super() call or the base order is incorrect. Changing the order of bases in the class definition changes the MRO. For instance, class Derived(Right, Left) would make Right appear before Left in the MRO, changing which method is called.

You can also use the __mro__ attribute to see the full tuple, which is useful when you need to programmatically check the hierarchy. In complex projects, adding a small assertion that verifies a particular class appears before another can prevent subtle bugs.

Runtime Behavior and Maintainability

The MRO is computed once at class definition time, so there is no runtime lookup cost per method call. The method resolution itself is a dictionary lookup on the class, which is fast. The performance impact of diamond inheritance is negligible in normal applications.

The real cost is maintainability. A diamond hierarchy with many cooperating classes is harder to reason about than a linear one. When a method is overridden in multiple places, you must trace the MRO to understand which implementation runs. This becomes more difficult as the hierarchy grows.

To keep the code maintainable, document the intended MRO order and ensure that every class in the diamond calls super() in overridden methods. Use mixins that are small and single-purpose. If a mixin's method does not need to call super(), it should still do so to preserve the chain, even if the call is a no-op. This cooperative discipline is the price of using diamond inheritance effectively.

python diamond inheritance: Practical Usage and Code Example | RYUSLOG DEV