← Back to blog
April 12, 2026·10 min read

The Diamond Problem, Demystified

PythonMROInheritance

If you've written a class that inherits from two parents, you've created a potential diamond. If those two parents share a common ancestor, the diamond is real, and Python has to decide: when your class calls a method, which version runs? The answer depends on the method resolution order, and the algorithm Python uses to compute it is more subtle than most developers expect.

The shape of the problem

         A
        / \
       B   C
        \ /
         D

  class D(B, C): pass
  class B(A): pass
  class C(A): pass

  When D calls a method defined in A, B, and C,
  which one runs?

The "diamond" refers to the shape of the inheritance graph. D inherits from both B and C. Both B and C inherit from A. When D looks up a method, there are multiple paths to A, and potentially multiple definitions along the way.

This isn't a corner case. It shows up in real frameworks. GUI toolkits, ORM systems, ML pipelines. Any codebase that uses mixins or multiple inheritance to compose behavior will eventually produce diamonds.

Why depth-first search fails

Before Python 2.3, method resolution used a simple depth-first left-to-right traversal. For the diamond above, that produced:

text
DFS order: D -> B -> A -> C -> A

Problem: A appears before C.
A method defined in A would be found before
a method defined in C, even though C is a
direct parent of D.

That breaks the intuition that a direct parent should take priority over a grandparent. If B and C both override a method from A, the DFS order finds B's version (correct, since B is listed first) but then finds A's version before C's. So A's version shadows C's, which is wrong. C is more specific than A.

C3 linearization

Python 2.3 switched to C3 linearization, an algorithm that guarantees three properties:

  • Children come before parents. D always appears before B, C, and A.
  • Left-to-right order is preserved. If you write class D(B, C), then B comes before C.
  • No class appears twice. The algorithm finds a single consistent ordering or rejects the hierarchy entirely.

The algorithm computes the MRO recursively and merges the results. For a class C with bases B1, B2:

text
MRO(C) = C + merge(MRO(B1), MRO(B2), [B1, B2])

The merge procedure:
  1. Look at the first element of each list.
  2. Pick one that doesn't appear in the tail
     of any other list.
  3. Add it to the result, remove it from all lists.
  4. Repeat until all lists are empty.
  5. If no valid pick exists, the hierarchy
     is inconsistent.

Walking through the diamond

Let's trace it for class D(B, C) where B(A) and C(A):

text
MRO(A) = [A]
MRO(B) = [B, A]
MRO(C) = [C, A]

MRO(D) = D + merge([B, A], [C, A], [B, C])

Step 1: Heads are B, C, B.
  Is B in the tail of any list?
  Tails: [A], [A], [C]. No.
  Select B. Remove from all lists.
  Remaining: merge([A], [C, A], [C])

Step 2: Heads are A, C, C.
  Is A in the tail of any list?
  Tail of [C, A] is [A]. Yes! Blocked.
  Try C instead.
  Is C in the tail of any list?
  Tails: [], [A], []. No.
  Select C. Remove from all lists.
  Remaining: merge([A], [A], [])

Step 3: Head is A.
  Not in any tail. Select A.

Result: [D, B, C, A]

A appears only once, at the end. Both B and C get checked before A. And B comes before C because that's the order declared in class D(B, C). This is the correct, intuitive order.

When C3 says no

Not every hierarchy has a valid C3 linearization. Consider:

python
class A: pass
class B(A): pass
class C(A, B): pass  # Puts A before B

# This raises:
# TypeError: Cannot create a consistent method
# resolution order (MRO) for bases A, B

The problem: C declares A before B, but B is a subclass of A. C3 requires that children come before parents, which means B should come before A. But C's declaration puts A first. These constraints contradict each other, so Python refuses to create the class.

This is a feature, not a limitation. A silent wrong ordering would be far worse than a loud error at class creation time. If you hit this, the fix is usually to reorder the bases to match the inheritance relationships.

Where you actually encounter diamonds

Diamonds appear more often than you'd expect:

  • Mixin composition: class MyView(AuthMixin, LoggingMixin, BaseView). If both mixins inherit from BaseView or a shared base, there's your diamond.
  • Framework extension: Django's class-based views use cooperative multiple inheritance heavily. A typical view might have four or five bases that all converge on View.
  • ML pipelines: Distributed training estimators that inherit from both a model base and a hardware-specific mixin, both sharing a common configuration interface.

In these cases, the MRO determines which version of a method runs. If you add a validate() method to a mixin and the base view also defines validate(), the MRO picks the winner. The answer depends on the order of bases in every class declaration across the chain.

Making it visible

The reason the diamond problem causes real bugs is that the resolution is invisible. Nothing in your editor shows you the computed MRO. Nothing highlights that your validate() method is shadowed by a mixin declared earlier in someone else's class.

You can check the MRO in a REPL (MyClass.__mro__), but that requires importing the class, which means running the code. PRISM computes it statically from source. No imports, no execution, no side effects.

If you work with multiple inheritance, try this: add a print(YourClass.__mro__) somewhere in your project and look at the actual resolution order. Chances are it'll surprise you at least once. That surprise is the kind of thing that turns into a bug at 2 AM on a Friday.

Try PRISM

See method resolution in real time.