How Python Solves the Diamond Inheritance Problem

The diamond inheritance problem occurs in object-oriented programming when a class inherits from two classes that share a common ancestor, potentially causing ambiguity over which method to execute. Python resolves this structural challenge deterministically through the Method Resolution Order (MRO), powered by the C3 Linearization algorithm. By enforcing a strict, predictable hierarchy and leveraging the dynamic dispatch of super(), Python ensures that ancestor methods are invoked in a predictable sequence without redundant calls or priority conflicts.

The Diamond Problem Structure

In multiple inheritance, a diamond architecture forms when:

  1. Class A defines a method.
  2. Classes B and C both inherit from A and optionally override that method.
  3. Class D inherits from both B and C via class D(B, C):.

Without a defined resolution strategy, a call to the overridden method from an instance of D creates ambiguity: should the runtime invoke B's implementation or C's implementation?

The C3 Linearization Algorithm

To resolve this ambiguity, modern Python (Python 3 and Python 2.3+ new-style classes) uses the C3 Linearization algorithm. This algorithm constructs a flat, ordered list of classes—the Method Resolution Order (MRO)—for any class hierarchy.

C3 Linearization enforces three core properties:

If an inheritance graph creates a contradiction that violates monotonicity or local precedence, Python raises a TypeError at class definition time rather than executing ambiguous code.

Cooperative Multiple Inheritance with super()

Python's super() function is the mechanism that traverses the MRO. Unlike similar keywords in languages like Java or C++, super() does not simply refer to a direct parent class; instead, it delegates calls to the next class in the MRO of the calling instance.

Consider the following implementation:

class A:
    def action(self):
        print("A.action")

class B(A):
    def action(self):
        print("B.action (start)")
        super().action()
        print("B.action (end)")

class C(A):
    def action(self):
        print("C.action (start)")
        super().action()
        print("C.action (end)")

class D(B, C):
    def action(self):
        print("D.action (start)")
        super().action()
        print("D.action (end)")

Executing D().action() yields:

D.action (start)
B.action (start)
C.action (start)
A.action
C.action (end)
B.action (end)
D.action (end)

In this flow, B's call to super().action() does not immediately invoke A. Because the instance being evaluated is of type D, the MRO dictates that C comes after B. Control passes to C, and only when C calls super().action() does execution reach the root ancestor A. This cooperative chaining prevents A from being executed multiple times.

Inspecting the Resolution Order

Developers can inspect the computed resolution order at runtime using either the mro() method or the __mro__ attribute on any class:

print([cls.__name__ for cls in D.mro()])
# Output: ['D', 'B', 'C', 'A', 'object']

Python method lookups strictly follow this sequence from left to right, executing the first matching implementation found along the chain.