Python lets a class inherit from more than one parent, which is handy when a subclass needs behavior from two different sources. The flip side is that lookups can get ambiguous: if two parents both define describe, which one wins? This lesson covers the syntax for multiple inheritance, the diamond problem, the rule Python uses to resolve it (method resolution order, or MRO), how cooperative super() makes it work, and where mixins fit in.
In single inheritance, every subclass has exactly one parent in the parentheses. Multiple inheritance just means listing more than one:
Laptop doesn't define any methods of its own, yet it has apply_discount (from DiscountableMixin) and shipping_cost (from ShippableMixin) and the __init__ it inherits from Product. Python walks the parent classes when it can't find a name on the subclass itself.
The order matters. class Laptop(Product, DiscountableMixin, ShippableMixin) and class Laptop(ShippableMixin, DiscountableMixin, Product) are not the same class. They share the same set of parents but the order changes how Python resolves names when more than one parent has a match. We'll come back to this when we look at MRO.
Multiple inheritance gets interesting when two parents share a common ancestor. The shape of the class graph looks like a diamond, with one class at the top, two in the middle, and one at the bottom:
Laptop inherits from both DiscountableMixin and ShippableMixin, and both of those inherit from Product. If Laptop calls a method that Product defines, which path does Python take to find it? If both middle classes override the same method, which one wins?
Here's the problem in code:
This is the desirable answer, each class in the chain contributes one piece, and Product.describe runs exactly once at the end. Languages without a clear rule for diamond inheritance either reject this setup or call Product.describe twice. Python avoids both traps with the MRO.
The mental hook: in a diamond, the shared ancestor (Product) should be visited once, after both children that depend on it. The MRO is the algorithm that decides exactly when "once, after both children" means.
When you call instance.method(), Python doesn't just guess where method lives. It walks a fixed list of classes called the method resolution order (MRO) and picks the first class in the list that defines the name. Every class has an MRO, and you can read it.
ClassName.__mro__ returns a tuple. ClassName.mro() returns the same thing as a list. Both show the order Python will use:
Read it top-to-bottom. When you call Laptop().describe(), Python looks for describe on Laptop first, then DiscountableMixin, then ShippableMixin, then Product, then object. The first class with a matching name wins.
A few details worth noticing:
object is always at the end. Every Python class inherits from object, even when you don't write it.The diagram shows the diamond from earlier flattened into a single chain. The two middle classes (DiscountableMixin and ShippableMixin) appear once, in the order they were listed in Laptop's parents. Product appears once, after both. object sits at the very end.
Cost: MRO lookup is O(number of classes in the chain), but Python caches method lookups inside each class, so repeated calls to the same method are effectively O(1) after the first one. You don't pay the walk on every call.
Python builds the MRO using an algorithm called C3 linearization. The full algorithm has formal rules, but the intuition is small enough to hold in your head:
C3 walks the class graph and produces the unique ordering that satisfies all three rules. If no such ordering exists (because two rules contradict each other), Python refuses to create the class and raises TypeError at class-definition time.
Here's an order that C3 can't resolve:
X wants A before B. Y wants B before A. Z inherits from both, so the MRO would have to honor both orderings at once, which is impossible. C3 catches the contradiction and rejects the class.
You almost never hit this in practice with well-designed hierarchies. When you do, it usually means the design is trying to combine classes in incompatible ways and you should rethink the structure rather than fight the algorithm.
For the diamond case from earlier, C3 gives Laptop -> DiscountableMixin -> ShippableMixin -> Product -> object. Notice that Product waits until after both DiscountableMixin and ShippableMixin have appeared, which satisfies rule 1 (a class comes before its parents) for both middle classes at once. That's the property that makes the diamond resolve cleanly.
super(): Each Parent Must Pass the BatonThe diamond example worked because each describe called super().describe(). That pattern is called cooperative multiple inheritance: every class in the chain calls super() so the call walks through the entire MRO instead of stopping at one parent.
Inside DiscountableMixin.describe, super() doesn't mean "my parent class". It means "the next class after me in the MRO of whatever object I'm bound to". For a Laptop instance, the MRO is Laptop -> DiscountableMixin -> ShippableMixin -> Product -> object, so super() from inside DiscountableMixin resolves to ShippableMixin, not to Product. That's how Product.describe ends up running after both mixins, even though DiscountableMixin inherits from Product directly.
The same DiscountableMixin.describe behaves differently depending on what class it's called from. On a Laptop it forwards to ShippableMixin. On a plain DiscountableMixin instance, it would forward to Product. That's the cooperative part: each class trusts the MRO to deliver the call to the right place next, instead of hard-coding a parent name.
Two rules to make cooperative super() work:
Product.describe doesn't call super().describe(), the chain stops there, which is what we want when Product is the shared base. But if you put a class in the middle that forgets to call super(), the classes after it in the MRO never run.DiscountableMixin.describe(self) passes no arguments to super().describe(), but ShippableMixin.describe(self, verbose=False) expects a keyword argument, the call breaks. Cooperative chains usually accept and forward *args, **kwargs so each class can pick out the arguments it cares about.Here's a working __init__ chain that respects both rules:
Each __init__ picks the keyword arguments it cares about and forwards the rest with **kwargs. The super().__init__(**kwargs) line carries the leftover arguments down the MRO. The final stop is object.__init__, which accepts no extra arguments, which is why every class in the chain has to consume its own keywords by the time the call reaches object.
Cost: Cooperative super() is paying for itself at every level of the chain, one Python call per class in the MRO. For chains with two or three classes it's nothing. For deep mixin stacks you'll measure it, but you'll measure it as "still fast enough" almost always.
A mixin is a class designed to add one focused capability to other classes, not to stand on its own. Mixins follow a simple shape: small, no state of their own (or very little), and named after what they add, often ending in Mixin or able.
The mixins from earlier (DiscountableMixin, ShippableMixin) are typical. Each owns one method, has no __init__ for state of its own (or accepts only what it needs), and doesn't try to be useful on its own. You don't write DiscountableMixin() directly; you mix it into a Product subclass.
A slightly fuller example:
Book doesn't define any methods. It composes capabilities from four parents: storage (Product), tax, gift wrap, and reviews. If you want a DigitalBook that's reviewable and taxable but has no shipping or gift wrap, you list a different set of parents. If you want to add a FeaturedMixin that bumps the price in search rankings, you write the mixin once and use it in any product that needs it.
A few practical rules when writing mixins:
| Do | Don't |
|---|---|
| Keep each mixin focused on one capability | Bundle five unrelated methods into one mixin |
Document what attributes the mixin expects (Expects self.price) | Assume the consumer knows what fields you'll touch |
Make __init__ accept *args, **kwargs and call super().__init__(*args, **kwargs) | Hard-code a parent class in super() |
| Name the mixin after the capability it adds | Name it like a noun that sounds like a real entity |
The last point matters more than it sounds. DiscountableMixin reads as "this thing can be discounted", which tells you it's an add-on. Pricing reads like a standalone module or class, which makes a future reader wonder why a Laptop "is a" Pricing. Good names tell readers which classes are domain types and which are pluggable behaviors.
Multiple inheritance is a sharp tool. It solves real problems when the parents are orthogonal (different axes of behavior, no overlap), but it makes code harder to follow when the parents step on each other.
Reach for it when:
Avoid it when:
Product and a Customer is almost always wrong; the right answer is composition (a Product has a Customer who reviewed it).When in doubt, prefer composition: hold an instance of the other class as an attribute instead of inheriting from it. Polymorphism often gives you the flexibility you wanted from multiple inheritance without any of the MRO complexity.
Same capabilities, no diamond, no MRO to reason about. The trade-off is that calls go through one more dot (book.inventory.add_stock instead of book.add_stock), which is usually a fair price for the simpler structure.
10 quizzes