Command Palette

Search for a command to run...

Hectal
PHASE 8Intermediate ~25 min· topic 6 of 12

Topic 8.6

Multiple Inheritance, Mixins and the MRO

In one line

A class can have several parents. Python decides which parent's method runs by following the method resolution order (MRO), computed by the C3 linearisation rule, and super() follows that same order. Mixins use this to add small, focused abilities to many classes.

Think of it like this

A person who is both a student and an employee. They follow the rules of both roles. If both roles have a rule for "what time do you arrive?", you need a way to decide which answer wins, and you probably want a fixed order, such as "check the student rules first, then the employee rules". Python's MRO is that fixed order for classes.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Multiple inheritance
A class with more than one parent class.
MRO (method resolution order)
The ordered list of classes Python searches for an attribute or method. Shown by Class.__mro__.
C3 linearisation
The algorithm that builds the MRO: children before parents, base order kept.
Diamond problem
Two parents sharing one grandparent. The question: whose method runs, and does the grandparent run twice?
Cooperative `super()`
Every class in a hierarchy calls super(), so all of them run once in MRO order.
Mixin
A small class that adds one capability to others by being listed as an extra parent.
`__mro__`
The attribute holding a class's MRO as a tuple.
`**kwargs` forwarding
Passing keyword arguments up the chain so each class takes what it needs and forwards the rest. **kwargs collects extra keyword arguments into a dictionary (Topic 5.3).

Step by step

01Several parents

List the parents in brackets, separated by commas. class D(B, C) means D has two parents. The order you write them is part of the design: it decides who is asked first.

02The diamond

In the example, B and C both inherit from A, and D inherits from both. Draw it and you get a diamond with A at the bottom and D at the top.

The diamonddiagram
Rendering diagram…

03Read the MRO

D.__mro__ is (D, B, C, A, object). Python looks for a method in that exact order. Notice A comes after both B and C, because C3 puts a parent after all of its children.

Print __mro__ whenever behaviour confuses you. It is the single source of truth.

main.pywhole filepython
print([cls.__name__ for cls in D.__mro__])
# ['D', 'B', 'C', 'A', 'object']

04super() walks the MRO

D.hello calls super().hello(). The next class after D is B, so B.hello runs. Its super().hello() goes to the next class after B in D's MRO, which is C, not A. Then C calls A. The result ['D', 'B', 'C', 'A'] shows every class ran exactly once, in MRO order.

This is why super() is not simply "the parent": from inside B, the next class depends on the object's full MRO.

05Order of bases changes the result

class E(C, B) gives E, C, B, A, object. Swap the bases and the search order swaps. If both B and C define the same method, the first listed wins.

06Impossible orders raise TypeError

class Bad(A, B) with B a subclass of A cannot satisfy both promises, so Python stops at class creation with Cannot create a consistent method resolution order. The fix is to list the more specific class first: class Bad(B, A).

07Mixins: small abilities

A mixin adds a feature without being a full is-a parent. JsonMixin needs only vars(self), so it works with any class. Listing mixins first gives them priority over the base.

Cooperative constructors use keyword arguments: each class takes the arguments it needs and passes the remaining ones to super().__init__(**kwargs). That is how Person(name="Asha", age=30) reaches both Named and Aged.

Try it yourself

  1. 1

    Predict the MRO

    Before running, write down the MRO of class E(C, B). Then run the first example and compare. If you were wrong, redraw the diamond and apply the two promises: children before parents, base order kept.

  2. 2

    Make a class cooperate badly

    In C.hello, replace super().hello() with A.hello(self) and run D().hello(). Nothing seems to change, because C calls A directly and A still runs. The problem appears when another class sits between C and A in the MRO: a class that skips super() leaves every class after it unvisited.

  3. 3

    Add a mixin

    In the second example create class PrettyMixin with a method shout returning self.name.upper() and add it to the bases of ToolCall. Print call.shout().

Code & diagrams

The diamond and its MRO New tab

Swap B and C in class D(B, C) and watch the order change. Then call D().hello() again.

Sign in to run this example in your browser.

Expected output

['D', 'B', 'C', 'A', 'object']
['D', 'B', 'C', 'A']
['B', 'A', 'object']
['E', 'C', 'B', 'A', 'object']
TypeError: Cannot create a consistent method resolution order (MRO) for bases A, B
Mixins for a tool call, and cooperative __init__ New tab

ToolCall is the kind of small record an AI agent passes around. Add a new mixin and list it in the bases.

Sign in to run this example in your browser.

Expected output

ToolCall(name='search', query='python mro')
{"name": "search", "query": "python mro"}
['ToolCall', 'JsonMixin', 'ReprMixin', 'Base', 'object']
Asha 30
How super() hops through the diamonddiagram
Rendering diagram…

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Ask for an impossible order

List a parent before its own child in the bases.

main.pywhole filepython
class A:
    pass


class B(A):
    pass


class C(A, B):
    pass
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 9, in <module>
class C(A, B):
pass
TypeError: Cannot create a consistent method resolution order (MRO) for bases A, B

Myth vs fact

Myth

With multiple inheritance, the grandparent method runs twice.

Fact

The MRO lists each class once, and cooperative super() visits each once. That is the point of C3.

Myth

super() always calls the direct parent.

Fact

It calls the next class in the MRO of the object's class. In a diamond, that can be a sibling.

Myth

Multiple inheritance is always bad.

Fact

Deep, tangled hierarchies are bad. Small, focused mixins are a normal tool in Python (Django and many libraries use them). Keep them few and simple.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The MRO belongs to the instance's class, so the same method in B can call a different super() target depending on whether the object is a B or a D. This is why super() is a runtime lookup, and why class-specific code in B cannot assume its parent is A.

  • ▸

    Cooperative __init__ needs a convention: accept **kwargs, consume your own arguments, pass the rest up. The last class (object) accepts none, so unconsumed arguments raise TypeError, which usefully flags typos.

  • ▸

    Mixins should be stateless or have no __init__, and should not assume a particular base class beyond a documented attribute or method. If a mixin needs self.name, say so in its docstring (or type it with a Protocol, Topic 8.10).

  • ▸

    Cooperative super() only works if the methods have compatible signatures, which is why cooperative __init__ uses **kwargs. The usual failure is a class that calls super().__init__(name) positionally while the next class in the MRO expects different arguments: the error shows up far from the cause. Print __mro__ first.

  • ▸

    Mixins and __slots__ (Topic 8.12): two parents that both add instance storage fail with TypeError: multiple bases have instance lay-out conflict. Give mixins an empty __slots__ = () so they add no layout, and only one base carries real slots.

  • ▸

    In AI code, base classes such as callback handlers or model wrappers often use mixins for logging, retry or caching. Composition (Topic 8.11) is usually clearer when the behaviours need configuration.

Remember this

  1. 1

    Python allows class D(B, C): with more than one parent. This is multiple inheritance. Many languages ban it because of confusion; Python allows it and defines a clear rule so the confusion is manageable.

  2. 2

    The method resolution order (MRO) is the list of classes Python searches, in order, when you look up a method or attribute. You can see it with D.__mro__. The first class in the list that has the name wins. For single inheritance (one parent, as in Topic 8.5) it is just the chain up to object.

  3. 3

    Python builds the MRO with the C3 linearisation algorithm. Its two promises are: a class always comes before its parents, and the order of the bases you wrote is kept. For class D(B, C), where both B and C inherit from A, the MRO is D, B, C, A, object. Note that A comes after both B and C.

  4. 4

    That shape is the diamond problem: D inherits from B and C, which both inherit from A. In languages without a rule, it is unclear whether A's method runs once or twice. In Python, A appears once in the MRO, so it runs once, and the order is predictable.

  5. 5

    Recall from Topic 8.5: super() reaches the parent's version of a method. With several parents it does not mean "my parent"; it means "the next class in the MRO" after the current one. When every class in the diamond calls super().hello(), the calls travel D → B → C → A as a chain, and each class runs once. This is cooperative inheritance, and it works only if every class cooperates by calling super().

  6. 6

    Some orders are impossible. class Bad(A, B) where B already inherits from A asks for A before its own child B, which breaks the first promise. Python refuses with TypeError: Cannot create a consistent method resolution order.

  7. 7

    A mixin is a small class that adds one ability and is not meant to stand alone: JsonMixin adds to_json, ReprMixin adds a readable __repr__. You combine them with a real base class: class ToolCall(JsonMixin, ReprMixin, Base). Put mixins to the left of the main base so their methods are found first. Keep mixins small, with no __init__ of their own where possible.

Explain it without notes

01

What is the MRO, and how do you see it?

02

What are the two rules of C3 linearisation?

03

What does super() really do in a multiple-inheritance hierarchy?

04

What is a mixin, and how should you design one?

Practice

01

Write classes A, B(A), C(A) and D(B, C) each with name() returning its own letter. Print D().name() and the MRO names. Explain why.

02

Write a ShoutMixin with a method shout() that returns self.speak().upper() + "!", and a Dog class with speak(); combine them as LoudDog(ShoutMixin, Dog) and print LoudDog().shout().

03

Write a hierarchy where B and C both inherit A and each calls super().__init__(), appending its letter to a list stored in A, to show each runs once. Create D(B, C) and print the list.

Trade-offs

  • ↔

    Mixins add abilities cheaply, but spread behaviour across files. To understand one method you may need the MRO. Few, small, well-named mixins stay readable.

  • ↔

    **Cooperative super() runs every class once, but needs every class to cooperate.** One class calling its parent directly can silently skip others. Composition avoids this by making calls explicit.

Done when you can

  • Done when you can print an MRO and explain why each class sits where it does.

  • Done when you can explain the two rules of C3 linearisation.

  • Done when you can explain that super() means the next class in the MRO.

  • Done when you can write a small mixin and place it correctly in the bases.

  • Done when you can recognise and fix the "consistent method resolution order" error.