Deep Engineering
Intermediate·Published·3.11 · 3.12 · 3.13 · 3.14·25 MIN

MRO and multiple inheritance: C3, the diamond, and where super() really goes

The method lookup order is not “left to right, depth first” but the C3 linearization, and it follows from a single merge rule. The same rule gives both the diamond, where the common ancestor goes to the tail, and the TypeError with which Python refuses to create a class. And super() goes not to the parent but to the next class in the instance's MRO — so the same method calls different neighbours in different hierarchies.

Full technical treatment

TL;DR

The method lookup order is a list, and every class has its own: __mro__. Under single inheritance it is obvious — the class, its parent, its grandparent. Under multiple inheritance it is built by the C3 algorithm, which has one rule: take the head of the first list if it is not in the tail of any other, otherwise the head of the next one. Two facts follow from that rule and are worth knowing by heart. In the diamond D(Left, Right) the common ancestor goes to the tail: D, Left, Right, Base, object, and a method Left lacks is taken from Right, not from Base. And if the bases demand opposite orders, there is no good head, and Python refuses to create the class: TypeError: Cannot create a consistent method resolution order (MRO).

Beyond that is what separates knowing from having read. super() goes not to the parent but to the next class in the instance's MRO. The method Left.greet is written once, yet for a Left instance its super() goes to Base, and for a D instance to Right, which Left does not inherit and knows nothing about. That is why in a diamond where everyone calls super() each method runs exactly once. And that is why the chain ends silently at a class that does not call super(): the neighbours are in the MRO and never run, with no error. In CPython the length of the MRO barely affects a repeated lookup: the type attribute cache answers in 10.8–11.9 ns for lengths from 2 to 51 (3.13.7); without the cache the lookup is linear.

Where to start
Before this lesson it is enough to understand
  • how a class is declared and inherited from another;
  • that a method the class lacks is looked up in its parent;
  • that a class can inherit from several at once: class D(Left, Right).
You do not need to know in advance
  • __mro__, the C3 linearization, a conflict in the order of bases;
  • super() in a diamond, a cooperative __init__, the __class__ cell, the type attribute cache.

Base: every class has a list its methods are looked up in

When you write obj.greet(), Python looks for greet not “somewhere in the hierarchy” but along a specific list of classes, in order, and takes the first match. The list sits on the class in the __mro__ attribute — the method resolution order.

Under single inheritance it is obvious:

PYTHON
class Base:
    def greet(self):
        return ["Base"]
 
class Left(Base):
    def greet(self):
        return ["Left"] + super().greet()
 
print(Left.__mro__)   # (Left, Base, object)

The class, its parent, the parent's parent — and object at the end, which everything inherits from. The Junior-level answer to “what is the MRO” is exactly this: it is the order in which Python looks an attribute up across the classes of a hierarchy; you can see it in Class.__mro__.

The question gets interesting when there is more than one parent, and especially when they share an ancestor — a diamond.

Mechanism 1: C3 — a merge with one rule

language contractThe C3 rule has been part of the language since Python 2.3; the order and the text of the refusal were checked by running bench/mro/order.py on 3.11–3.14.

Intuition says “left to right, depth first”: first the whole Left branch with its ancestors, then the whole Right branch. For the diamond that gives D, Left, Base, Right, Base — and Base's method would win over Right's. Python does not do that:

PYTHON
class Right(Base):
    def greet(self):
        return ["Right"] + super().greet()
 
class D(Left, Right):
    def greet(self):
        return ["D"] + super().greet()
 
print([c.__name__ for c in D.__mro__])
# ['D', 'Left', 'Right', 'Base', 'object']

The common ancestor Base went to the tail, after both of its descendants. Where that comes from is easiest to see by hand. C3 merges several lists: the parents' orders — Left, Base, object for Left and Right, Base, object for Right — and the list of bases itself, Left, Right. There is one rule: take the head of the first list if it is not in the tail of any other list; otherwise try the head of the next one. The class taken is struck out of every list, and the step repeats.

The script bench/mro/order.py does exactly that, prints the merge step by step and checks the result against the interpreter's __mro__ — it matched on seven hierarchies out of seven:

1) C3 step by step: class D(Left, Right), common ancestor Base
   step 1: [Left Base object]  [Right Base object]  [Left Right]  ->  take Left
   step 2: [Base object]  [Right Base object]  [Right]  ->  take Right
   step 3: [Base object]  [Base object]  ->  take Base
   step 4: [object]  [object]  ->  take object
   by hand:         ['D', 'Left', 'Right', 'Base', 'object']
   D.__mro__:       ['D', 'Left', 'Right', 'Base', 'object']

   hand-written C3 matched __mro__ on 7 hierarchies of 7

The whole diamond is decided at the second step. Left's list comes first, and its head is now Base — but Base sits in the tail of Right's list. It cannot be taken until Right is. That is how the rule expresses what the order must satisfy: a class comes before its ancestors (hence Base after both descendants) and the order of the bases in the declaration is kept (hence Left before Right).

Formally, the documentation writes the same thing in one line:

the linearization of C is the sum of C plus the merge of the linearizations of the parents and the list of the parents

— The Python 2.3 Method Resolution Order

And the merge by the very rule carried out by hand above:

take the head of the first list, i.e L[B1][0]; if this head is not in the tail of any of the other lists, then add it to the linearization of C and remove it from the lists in the merge

— The Python 2.3 Method Resolution Order

This strictness has a name — monotonicity: if C1 comes before C2 in a class's order, the same holds in the order of every subclass. Adding a subclass cannot accidentally reorder the methods of its ancestors.

Mechanism 2: when there is no order, there is no class

The merge does not always succeed. Two classes with the opposite order of the same bases — and at some step there is no good head:

2) when there is no order
   class Impossible(XY, YX) -> TypeError: Cannot create a consistent method resolution order (MRO) for bases X, Y
   class Backwards(Base, Left) -> TypeError: Cannot create a consistent method resolution order (MRO) for bases Base, Left
   merge by hand for Impossible(XY, YX):
   step 1: [XY X Y object]  [YX Y X object]  [XY YX]  ->  take XY
   step 2: [X Y object]  [YX Y X object]  [YX]  ->  take YX
   step 3: no good head among X, Y

XY demands that X come before Y, YX the reverse. The head of each list lies in the tail of the other, and Python does not pick a “reasonable” option — it refuses: it is impossible to construct the merge, Python 2.3 will refuse to create the class C and will raise an exception.

The second example is shorter and looks innocent: class Backwards(Base, Left). An ancestor is declared before its own descendant — and C3 requires the descendant to come before the ancestor. The fix is to swap the bases.

What matters is when this happens: when the class statement runs, that is, when the module is imported. It never gets as far as a method call — the module will not load.

Mechanism 3: super() is the next in the instance's MRO, not the parent

language contractThe behaviour of super() is defined by the built-in functions documentation; the run of bench/mro/order.py, sections 3–5, is identical on 3.11–3.14.

The familiar phrasing is “super() calls the parent's method.” The documentation says something else:

The search starts from the class right after the type.

— Built-in Functions — super

“After” in which list? In the MRO of the object whose method was called, not of the class where super() is written. The precise wording is this: super(Left, obj) returns neither a class nor a call but a proxy — an object through which attribute lookup goes along type(obj).__mro__, starting with the class after Left. super() itself calls nothing: the call is the .greet() on the method found through it. The short “super() goes to the next in the instance's MRO” is a compressed form of this formula. That is what the diamond's behaviour is made of:

3) where super() inside Left.greet goes
   Left().greet()  -> ['Left', 'Base']
   D().greet()     -> ['D', 'Left', 'Right', 'Base']

Left.greet is written once. For a Left instance, the next after Left is Base. For a D instance, the next after Left is Right, which Left does not inherit and knows nothing about. Each method in the diamond ran exactly once, Base at the very end. That is a guarantee of the cooperative chain, not of the language: it holds as long as all its participants call super(). It is why super() is built this way: calling the parent by name would run Base twice, once from each branch.

The flip side is that the chain relies on everyone calling super(). All it takes is one class that does not call it:

4) one class does not call super()
   Broken.__mro__:  ['Broken', 'QuietLeft', 'Right', 'Base', 'object']
   Broken().greet() -> ['Broken', 'QuietLeft']

Right and Base are in the MRO but never ran. No error, no warning: the chain simply ended at QuietLeft. For Python this is not an error but an ordinary method — and at the end of a chain it is exactly the intent: Base.greet here does not call super(), and the chain ends there. It becomes a bug when the hierarchy is meant to be cooperative and a class in its middle drops out of the contract: if Right.__init__ opened a connection or registered the object, that did not happen.

A cooperative __init__

For __init__ this gives a practical rule. No class in the chain knows what comes after it — so none knows what arguments the next one needs. The documentation states the requirement like this:

Good design dictates that such implementations have the same calling signature in every case

— Built-in Functions — super

In practice that means keyword arguments: each class takes its own and passes the rest along.

PYTHON
class Named:
    def __init__(self, *, name, **rest):
        self.name = name
        super().__init__(**rest)
 
class Sized:
    def __init__(self, *, size, **rest):
        self.size = size
        super().__init__(**rest)
 
class Item(Named, Sized):
    pass
5) cooperative __init__: each takes its own, passes the rest on
   Item.__mro__: ['Item', 'Named', 'Sized', 'object']
   Item(name='box', size=3) -> name='box', size=3
   the extra argument colour reached object -> TypeError: object.__init__() takes exactly one argument (the instance to initialize)

The last line is useful, not annoying: an argument nobody took reaches object.__init__ and fails. A typo in an argument name is not swallowed silently.

Deeper: how super() knows the class, and what a long MRO costs

measured observationbench/mro/order.py, section 6, and bench/attr-lookup/mro_cache.py, CPython 3.13.7 and 3.14.7. The times were taken on one machine; what matters is not the nanoseconds but the slope: whether cost grows with the length of the MRO.

super() with no arguments is not magic but a cell. Two levels are worth telling apart here. The language contract, set by PEP 3135: super() with no arguments works in a function declared in a class body and takes that class. How exactly that is done is a CPython implementation detail, and the run shows it: the compiler puts a hidden free variable __class__ into such a function — if the function uses super() or __class__. The PEP puts it more broadly: Every function will have a cell named class that contains the class object that the function is defined in.

6) super() with no arguments: the __class__ cell
   Left.greet.__code__.co_freevars: ('__class__',)
   cell contents: Left
   method without super() or __class__, co_freevars: ()
   a function declared outside the class -> RuntimeError: super(): __class__ cell not found

co_freevars and the cell itself are what CPython shows, not a promise of the language; code should rely on the contract. From it comes super()'s first argument: the class the method is written in. The second is self. The MRO is taken from type(self), and the search starts after __class__. The same fact gives the limitation: a function declared outside and attached to the class later has no cell, and super() with no arguments fails in it. There you write the explicit form super(Left, self).

The length of the MRO barely affects a repeated call. A lookup along the list of classes is linear, and without the cache that shows: each level adds 3.31 ns on 3.13.7, and at length 51 an access costs 180.8 ns. But a type has an attribute cache: it answers a repeated access without walking the list at all.

PY 3.13.7 | best of 100 interleaved rounds of 20000 accesses

  __mro__ length    hot cache     invalidated     (cost of assignment)
              2        10.8 ns          18.7 ns               26.7 ns
              6        10.9 ns          29.2 ns               27.4 ns
             21        11.0 ns          87.4 ns               26.8 ns
             51        11.9 ns         180.8 ns               26.8 ns

  per MRO level without cache: +3.31 ns
  per MRO level with cache:    +0.02 ns
  at length 51: 181 ns without cache, 12 ns with

The cache is invalidated by assigning to the class — which is why the comparison invalidates it on purpose on every iteration. In ordinary code classes do not change after import, and the left column is what applies. The cache is covered in detail in the article on what happens on o.x.

How to answer in an interview

Short answer: the MRO is the order in which Python looks an attribute up across the classes of a hierarchy; it lives in Class.__mro__ and is built by the C3 algorithm. C3 puts a class before its ancestors and keeps the order of the bases, so in the diamond D(Left, Right) the common ancestor goes to the tail: D, Left, Right, Base, object. And super() is a proxy: lookup through it goes along the instance's MRO, starting after the class where the method is written, and that is not necessarily the parent.

That is enough to answer correctly. Beyond it is what you add if the interviewer digs.

If the interviewer digs deeper

Show the diamond and two outputs of the same method: for a Left instance its super() goes to Base, for a D instance to Right. It is one line of code and the most convincing proof that “super is the parent” is wrong. The consequence follows: in a cooperative hierarchy everyone calls super(), and arguments are passed as keywords with **rest, because the next in the chain is not known in advance. Then each method in the chain runs exactly once — a property of that contract, not a rule of Python.

About the refusal, be precise: TypeError: Cannot create a consistent method resolution order arises when the class is defined, that is, on import, for example because an ancestor is declared before its descendant — class C(Base, Child). The fix is to swap the bases.

And if cost comes up — in CPython the length of the MRO barely affects a repeated call: the hot type attribute cache answers in 10.8–11.9 ns for lengths from 2 to 51 (3.13.7). The linear growth, 3.31 ns per level, appears only when the cache is invalidated by assigning to the class.

Next they ask

Next they ask

How do you call a specific ancestor's method bypassing the MRO?

Short answer

Explicitly: Base.greet(self). It is an ordinary function taken from the class, with self passed by hand. But in a cooperative hierarchy this breaks the main property of super() — Base will run twice if the super() chain from the neighbouring branch reaches it too.

Next they ask

Why does super() work without arguments?

Short answer

The compiler puts a __class__ cell holding the class the method is declared in into a method that uses super(), and self is the method's first argument. A function attached to the class from outside has no cell: super() in it fails with RuntimeError: super(): __class__ cell not found, and you need the explicit form super(Left, self).

Next they ask

What happens if one class in the chain does not call super()?

Short answer

The chain ends there, and the classes after it in the MRO do not run — silently. In the run, Broken(QuietLeft, Right) outputs ['Broken', 'QuietLeft']: Right and Base are in the MRO and never called. Python treats this as an ordinary method; it becomes a problem only if the hierarchy is meant to be cooperative.

Common misconceptions

Claim

super() calls the parent class's method

Actually

It returns a proxy, and lookup through it goes along the INSTANCE's MRO, starting after the class where the method is written. For Left().greet() that really is the parent Base, but for D().greet() the same super() in Left goes to Right — a neighbour Left does not inherit. The documentation: The search starts from the class right after the type.

Claim

The MRO is a depth-first, left-to-right traversal

Actually

A depth-first traversal would give D, Left, Base, Right, Base for the diamond, and Base's method would win over Right's. C3 moves the common ancestor to the tail: D, Left, Right, Base, object. There is one rule — a class comes before all of its ancestors, and the order of the bases from the declaration is kept.

Claim

Python can always work out an order, somehow if need be

Actually

It cannot and does not try. If the bases demand opposite orders — class Impossible(XY, YX), or an ancestor before its descendant, class C(Base, Left) — Python fails with TypeError: Cannot create a consistent method resolution order (MRO) right when the class is defined.

Claim

A long hierarchy means a slow method call

Actually

In CPython with a hot type attribute cache, no: 10.8 ns at an MRO length of 2 and 11.9 ns at 51 (3.13.7). Linear growth exists only without the cache — 3.31 ns per level — and the cache is invalidated by assigning to the class, which ordinary code does not do after import.

Claim

If a class does not call super(), Python will say so

Actually

Silently — for Python it is an ordinary method. The classes after it are in the MRO and do not run: in the run Broken().greet() returned ['Broken', 'QuietLeft'], even though Right and Base are in its MRO.

Version history

VersionChangeWhat it means for code
2.3C3 becomes the MRO algorithm for new-style classes. The algorithm itself was not invented for Python — it comes from the Dylan language (Barrett et al., 1996). Simionato's document works through the 2.2 rule for new-style classes on an example and shows that it broke monotonicity — which is why it was replaced.
3.0PEP 3135: super() with no arguments. It rests on the __class__ cell the compiler puts into functions declared in a class body. Old-style classes are gone — C3 for everyone.
3.13The C3 refusal TypeError text is printed on one line. Verified: on 3.11.15 and 3.12.3 the message contains a line break — Cannot create a consistent method resolution and order (MRO) for bases X, Y come out as two lines. It does not affect behaviour, but it breaks tests that match the exception text as a whole.
3.14A metaclass that overrides mro() deprives its class of the type attribute cache — even if it returns exactly super().mro(). Measured by bench/attr-lookup/mro_override.py: 15.3 / 63.8 / 118.4 ns at lengths 2 / 21 / 51 against 6.5 / 6.4 / 6.4 ns for an ordinary class; on 3.13.7 there is no such effect. The C3 order itself did not change.

Practice

Two exercises. Answer first, then check against the real output: in both, the correct answer comes from a run of the script rather than being assigned.

Practice · predict the output

A diamond: A and B inherit Base, C inherits A and B. The methods of A and B call super(). What does this code print?
class Base:
  def hello(self):
      print("Base")


class A(Base):
  def hello(self):
      print("A")
      super().hello()


class B(Base):
  def hello(self):
      print("B")
      super().hello()


class C(A, B):
  pass


C().hello()

Practice · estimate

Accessing the method o.m on an object whose MRO has 2 classes, and on one whose MRO has 51; the method is declared at the very top. How many times more expensive is the second?
times

Check yourself

Question 1 of 4

class D(Left, Right), where Left and Right inherit Base. Left has no method m; Right and Base do. Whose m does D().m() call?

How it was measured

The numbers in this lesson come from these scripts. Each one opens right from here — together with the record of its run: what it ran on, what came out, and with what spread.

Sources & further reading

4 SOURCES

  1. The Python 2.3 Method Resolution OrderOfficial documentation. The primary source for the rule that builds the MRO. Michele Simionato. The formula: “the linearization of C is the sum of C plus the merge of the linearizations of the parents and the list of the parents.” The merge rule: “take the head of the first list, i.e L[B1][0]; if this head is not in the tail of any of the other lists, then add it to the linearization of C and remove it from the lists in the merge.” And the refusal: “it is impossible to construct the merge, Python 2.3 will refuse to create the class C and will raise an exception.”https://docs.python.org/3/howto/mro.html
  2. Built-in Functions — superOfficial documentation. Where the claim that super() does not go to the parent comes from: “The search starts from the class right after the type,” with the example: “if __mro__ of object_or_type is D -> B -> C -> A -> object and the value of type is B, then super() searches C -> A -> object.” The rule for cooperative methods is on the same page: “Good design dictates that such implementations have the same calling signature in every case.”https://docs.python.org/3.14/library/functions.html#super
  3. PEP 3135 — New SuperPEP. Calvin Spealman, Tim Delaney, Lie Ryan; Final, Python 3.0. The document that introduced super() with no arguments, and the mechanism it rests on: “Every function will have a cell named __class__ that contains the class object that the function is defined in.” The run makes it precise: the cell appears in a function declared in the class body that uses super() or __class__; a method without them has none, and neither does a function attached to the class from outside — super() in it fails with a RuntimeError.https://peps.python.org/pep-3135/
  4. A Monotonic Superclass Linearization for DylanSource. Barrett, Cassels, Haahr, Moon, Playford, Withington, OOPSLA 1996. The paper where the C3 algorithm appeared — for the Dylan language, not for Python; Python adopted it in 2.3. Cited to name the property that makes C3 so strict: monotonicity — if C1 comes before C2 in a class's order, it does so in the order of every subclass too.https://doi.org/10.1145/236337.236343