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:
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:
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):
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:
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, BThe 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.