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.
**kwargscollects 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.
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.
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
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
Make a class cooperate badly
In
C.hello, replacesuper().hello()withA.hello(self)and runD().hello(). Nothing seems to change, becauseCcallsAdirectly andAstill runs. The problem appears when another class sits betweenCandAin the MRO: a class that skipssuper()leaves every class after it unvisited. - 3
Add a mixin
In the second example create
class PrettyMixinwith a methodshoutreturningself.name.upper()and add it to the bases ofToolCall. Printcall.shout().
Code & diagrams
Swap B and C in class D(B, C) and watch the order change. Then call D().hello() again.
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, BToolCall is the kind of small record an AI agent passes around. Add a new mixin and list it in the bases.
Expected output
ToolCall(name='search', query='python mro')
{"name": "search", "query": "python mro"}
['ToolCall', 'JsonMixin', 'ReprMixin', 'Base', 'object']
Asha 30Break 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.
class A:
pass
class B(A):
pass
class C(A, B):
passMyth 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
Bcan call a differentsuper()target depending on whether the object is aBor aD. This is whysuper()is a runtime lookup, and why class-specific code inBcannot assume its parent isA. - ▸
Cooperative
__init__needs a convention: accept**kwargs, consume your own arguments, pass the rest up. The last class (object) accepts none, so unconsumed arguments raiseTypeError, 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 needsself.name, say so in its docstring (or type it with aProtocol, 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 callssuper().__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 withTypeError: 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
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
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 toobject. - 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 bothBandCinherit fromA, the MRO isD, B, C, A, object. Note thatAcomes after bothBandC. - 4
That shape is the diamond problem:
Dinherits fromBandC, which both inherit fromA. In languages without a rule, it is unclear whetherA's method runs once or twice. In Python,Aappears once in the MRO, so it runs once, and the order is predictable. - 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 callssuper().hello(), the calls travelD → B → C → Aas a chain, and each class runs once. This is cooperative inheritance, and it works only if every class cooperates by callingsuper(). - 6
Some orders are impossible.
class Bad(A, B)whereBalready inherits fromAasks forAbefore its own childB, which breaks the first promise. Python refuses withTypeError: Cannot create a consistent method resolution order. - 7
A mixin is a small class that adds one ability and is not meant to stand alone:
JsonMixinaddsto_json,ReprMixinadds 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
What is the MRO, and how do you see it?
What are the two rules of C3 linearisation?
What does super() really do in a multiple-inheritance hierarchy?
What is a mixin, and how should you design one?
Practice
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.
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().
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.