June 6, 2025
Python OOP Inheritance MRO

Python Inheritance, super(), and Method Resolution Order

Here is a scenario that will sound familiar. You spend a day writing a clean, solid class. It handles its responsibility well, the methods are clear, the logic is tight. Then a new requirement arrives: you need another class that does almost the same thing, just slightly different in a handful of spots. Your first instinct is to copy and paste the whole thing and tweak it. That works for about ten minutes, until both classes need the same bug fixed and you realize you have to do it twice, or until you forget and only fix it in one place. This is the exact problem inheritance was designed to solve, and it is one of the most fundamental ideas in object-oriented programming.

But Python inheritance is not just about code reuse. It is a mechanism for expressing relationships between concepts: a Car is-a Vehicle, a Dog is-an Animal, a CachedDatabase is-a Database. When you model those relationships correctly in code, you get reuse for free as a side effect. When you model them incorrectly, when you inherit just because it is convenient rather than because the relationship is real, you end up with classes that are coupled in confusing ways and break when you least expect them to.

And then there is multiple inheritance. Python, unlike Java, allows a class to inherit from more than one parent at the same time. That power comes with a serious question: when two parents define the same method, which one wins? The answer is the Method Resolution Order (MRO), and if you do not understand it, you will spend hours debugging behavior that seems random but is actually completely deterministic. Get the MRO wrong, and methods silently skip. Get it right, and you can compose complex behavior from simple, focused pieces without any of them stepping on each other.

We will walk through single inheritance first (the easy win), then tackle super() and why it is better than hardcoding parent class names. Then we will face the dragon: multiple inheritance, C3 linearization, and the diamond problem. By the end, you will inspect MRO like a pro and design mixin classes that do not explode.


Table of Contents
  1. Why Inheritance Exists
  2. Single Inheritance: Extending and Overriding
  3. Understanding super()
  4. Super() Under the Hood
  5. The Gotcha: super() Requires Cooperative Classes
  6. Multiple Inheritance and C3 Linearization
  7. MRO: The Diamond Problem Solved
  8. Visualizing C3 Linearization Step-by-Step
  9. The Diamond Problem
  10. Inspecting and Debugging MRO
  11. Mixin Classes: The Right Way to Do Multiple Inheritance
  12. isinstance() and issubclass(): Type Checking with Inheritance
  13. When NOT to Use Inheritance
  14. Method Resolution Order in Action: A Real Example
  15. Common Pitfalls and How to Avoid Them
  16. Key Takeaways

Why Inheritance Exists

Before we write a single line of code, it is worth being honest about what inheritance is actually for, because the Python community has learned some hard lessons about misusing it.

Inheritance models an is-a relationship. A Car is a Vehicle. A Labrador is a Dog is an Animal. When that relationship is genuine, inheritance gives you a powerful payoff: the child class automatically gets all of the parent's attributes and methods, and you only have to write what is new or different. Change a method in the parent, and every child class picks up that change instantly.

The deeper reason inheritance matters, though, is polymorphism. When you write a function that accepts an Animal and calls .speak() on it, that function does not need to know whether it received a Dog, a Cat, or a Parrot. Each subclass provides its own implementation of .speak(), and the right one is called automatically. This is the open-closed principle in action: your function is open for extension (you can add new animal types) but closed for modification (you never touch the function itself). For large codebases, this is transformative.

Where inheritance goes wrong is when people use it purely for code reuse without a genuine is-a relationship. If you find yourself inheriting from a class just because it has a method you want, that is almost always a sign that composition is the better tool. We will come back to that distinction at the end of this article, but keep it in mind as we go: inheritance is about relationships first, reuse second.


Single Inheritance: Extending and Overriding

Let us start simple. You have a Vehicle class, basic, functional.

python
class Vehicle:
    def __init__(self, make, model):
        self.make = make
        self.model = model
 
    def describe(self):
        return f"{self.make} {self.model}"
 
    def start(self):
        return "Engine starting..."

This is your base. It captures everything that every vehicle has in common: a make, a model, a way to describe itself, a way to start. Now you want a Car that is-a Vehicle but adds doors and overrides how it starts.

python
class Car(Vehicle):
    def __init__(self, make, model, doors):
        # Initialize parent first
        super().__init__(make, model)
        self.doors = doors
 
    def describe(self):
        # Override: add door info
        parent_desc = super().describe()
        return f"{parent_desc} ({self.doors}-door)"
 
    def start(self):
        # Override completely
        return f"Car engine starting... ({self.doors} doors unlocking)"

Notice the structure here. We call super().__init__(make, model) at the top of Car.__init__ before setting anything that is specific to Car. This ensures the parent class gets to initialize its own state properly before we layer on ours. Then for describe(), we call super().describe() to grab the parent's formatted string and extend it rather than duplicating the formatting logic. For start(), we override completely because a car's startup is different enough that the parent's version is not useful.

python
car = Car("Toyota", "Corolla", 4)
print(car.describe())
# Output: Toyota Corolla (4-door)
 
print(car.start())
# Output: Car engine starting... (4 doors unlocking)

The output is exactly what you would expect, and no code was copied. If Toyota changes how describe() formats the make and model, your Car class picks that up automatically because it delegates to super().describe() first.

Why call super().__init__(make, model) instead of Vehicle.__init__(self, make, model)?

Because super() is cooperative. If you later add another parent class, or if someone subclasses your Car, super() will navigate the inheritance chain correctly. Hardcoding Vehicle bypasses that chain, it is rigid and fragile.


Understanding super()

super() is deceptive. It does not literally mean "call the parent class." It means "call the next method in the resolution order."

This distinction feels academic in single inheritance, but it becomes critical the moment you add a second parent. Here is the basic pattern at work:

python
class Parent:
    def greet(self):
        return "Hello from Parent"
 
class Child(Parent):
    def greet(self):
        parent_greeting = super().greet()
        return f"{parent_greeting}, and hello from Child"
 
child = Child()
print(child.greet())
# Output: Hello from Parent, and hello from Child

The Child.greet() method calls super().greet(), which resolves to Parent.greet() and returns its string. Then Child appends its own contribution and returns the combined result. Clean, composable, and easy to follow.

super().greet() does not call Parent.greet() directly, it calls the next class in the MRO after Child. In this case, that is Parent. But in multiple inheritance, it might be something different entirely, which is where things get interesting.

Why this matters: If you are writing a mixin or a library class that others will subclass, using super() makes your code cooperative. Hardcoding parent class names breaks that contract and makes your class hostile to anyone who wants to build on top of it.


Super() Under the Hood

When you write super() inside a method, Python is not just looking up the parent class. It is doing something more precise: it is finding the current class in the MRO, then returning a proxy object that delegates method calls to the next class in that list.

The full form is super(CurrentClass, self), but since Python 3 you almost never need to write it that way, the zero-argument form super() works inside any method definition and Python fills in the class and instance automatically using __class__ cell variables that the compiler inserts. This is why super() only works inside methods; outside a method definition, there is no implicit class context for the compiler to capture.

What does this proxy object actually do? When you call super().greet(), Python looks at the MRO of type(self), not the class where super() is written, but the actual runtime type of the object. It finds CurrentClass in that MRO, then searches from the next position onward for a class that has greet. This is precisely what makes cooperative multiple inheritance work: each class's super() call is resolved against the full MRO of the final concrete type, not just the local inheritance chain. A mixin written in isolation will correctly delegate to whatever comes after it in any class that uses it, even classes that did not exist when the mixin was written.

The practical implication is that super() is context-sensitive. The same super().greet() call in Dog.greet() might resolve to Cat.greet() in one class hierarchy and to Animal.greet() in another, depending on what the final concrete type's MRO looks like. This is powerful. It also means you need to understand MRO to predict what super() will do, which is exactly why we are covering both topics together.


The Gotcha: super() Requires Cooperative Classes

Here is where things break in ways that are genuinely confusing until you know the rule.

python
class A:
    def speak(self):
        print("A speaks")
        # Forgot to call super()!
 
class B(A):
    def speak(self):
        print("B speaks")
        super().speak()
 
class C(B):
    def speak(self):
        print("C speaks")
        super().speak()
 
# MRO: C -> B -> A -> object
c = C()
c.speak()
# Output: C speaks, B speaks, A speaks

This works fine because even though A does not call super(), it is at the end of the chain. But watch what happens when we add super() to A and then try to use it in a multiple inheritance scenario:

python
class A:
    def speak(self):
        print("A speaks")
        super().speak()  # Now A tries to call super()
 
class B(A):
    def speak(self):
        print("B speaks")
        super().speak()
 
class C(B):
    def speak(self):
        print("C speaks")
        super().speak()
 
c = C()
c.speak()
# Traceback: AttributeError: 'super' object has no attribute 'speak'

Why? Because A's super().speak() tries to call object.speak(), which does not exist. In a multiple inheritance chain, A might not be at the end, there could be another class after it in the MRO. That class must implement speak(), or A should not call super().

Golden rule: In cooperatively-written hierarchies, every class that calls super() assumes there is a next class in the MRO that implements that method. If you are not sure there is one, either do not call super() or add a guard.

python
class A:
    def speak(self):
        print("A speaks")
        # Only call super if it's safe
        if hasattr(super(), 'speak'):
            super().speak()

The guard is a bit ugly, but it is the correct solution when you genuinely cannot know whether there will be a next class in the chain.


Multiple Inheritance and C3 Linearization

Now it gets real. You have multiple parents:

python
class Flyable:
    def move(self):
        return "Flying through the air"
 
class Swimmable:
    def move(self):
        return "Swimming through water"
 
class Duck(Flyable, Swimmable):
    pass
 
duck = Duck()
print(duck.move())
# Output: Flying through the air

Why did Flyable.move() win? Because of the Method Resolution Order (MRO). Python built a linear list of classes to search, in order. When it looks up move, it walks that list from left to right and uses the first class it finds that has the method.

python
print(Duck.__mro__)
# Output: (<class 'Duck'>, <class 'Flyable'>, <class 'Swimmable'>, <class 'object'>)

Python searches this list left-to-right. Duck does not have move(), so it checks Flyable, found. It never reaches Swimmable. The order of parents in class Duck(Flyable, Swimmable) directly determines which method wins.

The MRO is computed using C3 linearization, an algorithm that ensures three things:

  1. Child classes are checked before parents.
  2. Parent classes are checked in the order they were listed.
  3. Each class appears exactly once.

These three guarantees together prevent ambiguity and ensure that the MRO is always predictable, even in complex hierarchies.


MRO: The Diamond Problem Solved

The diamond problem is the classic challenge in multiple inheritance. Imagine four classes arranged like a diamond: A at the top, B and C both inheriting from A, and D inheriting from both B and C. Without a careful algorithm, you could end up calling A's methods twice, or skipping them entirely, or getting different behavior depending on which path you traversed.

Python's C3 linearization algorithm was specifically designed to eliminate this ambiguity. The name "C3" refers to the three properties the algorithm satisfies: it is consistent with local precedence order, it is monotonic (meaning a class always appears after its parents), and it satisfies the property of preserving extended precedence graphs. These mathematical guarantees mean that there is always exactly one valid MRO for any class hierarchy, and Python will compute it deterministically.

The key insight is that the diamond is only a problem if A's methods could run twice. With C3 linearization, every class appears exactly once in the MRO, so there is no double execution. When both B and C call super(), those calls chain through the single linearized list in order. B.speak() calls super, which resolves to C.speak(), which calls super, which resolves to A.speak(). Each method runs exactly once, in the right order, and the final result is assembled correctly. This is how Python turns a potentially explosive pattern into something you can reason about and rely on.


Visualizing C3 Linearization Step-by-Step

This is the part that demystifies MRO. Let us trace through a complex example:

python
class A:
    pass
 
class B(A):
    pass
 
class C(A):
    pass
 
class D(B, C):
    pass

How does Python compute D's MRO? The algorithm works by merging the MROs of the parent classes together with the list of parents, using a specific rule for which class to pick at each step.

Step 1: Start with D:

MRO(D) = [D] + ...

Step 2: D inherits from [B, C]. Compute MRO(B) and MRO(C):

MRO(B) = [B, A, object]
MRO(C) = [C, A, object]

Step 3: Merge these lists with D's parents:

MRO(D) = [D] + merge([B, A, object], [C, A, object], [B, C])

Step 4: The merge algorithm picks the first class from any list that does not appear as a tail in any other list. In this case:

  • B is a tail in [C, A, object]? No.
  • B is a tail in [B, C]? No (it is the head).
  • So pick B.
MRO(D) = [D, B] + merge([A, object], [C, A, object], [C])

Step 5: Continue:

  • A is a tail in [C, A, object]? Yes! Skip it.
  • C is a tail in [A, object]? No.
  • So pick C.
MRO(D) = [D, B, C] + merge([A, object], [A, object])

Step 6: Finish:

MRO(D) = [D, B, C, A, object]

Let us verify:

python
print(D.__mro__)
# Output: (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)

The result matches. The algorithm placed C before A because C is a child of A, so it must be searched first. And it placed B before C because D listed B first in its inheritance declaration.

Why does this matter? If you define D(C, B) instead of D(B, C), the MRO flips. Method resolution changes. Behavioral bugs appear that have nothing to do with the method implementations themselves, only with the order of parents in the class definition. Parent order is not just style, it is semantics.


The Diamond Problem

Here is the classic mess:

python
class Animal:
    def speak(self):
        return "Some sound"
 
class Dog(Animal):
    def speak(self):
        return super().speak() + " - Woof!"
 
class Cat(Animal):
    def speak(self):
        return super().speak() + " - Meow!"
 
class DogCat(Dog, Cat):
    pass
 
dogcat = DogCat()
print(dogcat.speak())
# Output: Some sound - Woof! - Meow!

Wait, both Dog and Cat contribute to the output even though DogCat does not define speak() at all. How? The answer is in the MRO and the cooperative super() calls in each class.

python
print(DogCat.__mro__)
# Output: (<class 'DogCat'>, <class 'Dog'>, <class 'Cat'>, <class 'Animal'>, <class 'object'>)

Here is the call chain:

  1. DogCat has no speak(), check Dog.
  2. Dog.speak() calls super().speak() → next in MRO is Cat.
  3. Cat.speak() calls super().speak() → next in MRO is Animal.
  4. Animal.speak() returns "Some sound".
  5. Wind back: Cat appends " - Meow!" → "Some sound - Meow!"
  6. Wind back: Dog appends " - Woof!" → "Some sound - Meow! - Woof!"

Without super(), this does not work. If Dog called Animal.speak() directly, Cat.speak() would never run. super() ensures every class in the chain gets a turn.

Visualized:

DogCat.speak()
    ↓ (MRO: Dog next)
Dog.speak(): calls super()
    ↓ (MRO: Cat next)
Cat.speak(): calls super()
    ↓ (MRO: Animal next)
Animal.speak(): returns "Some sound"
    ↑
Cat appends " - Meow!" → "Some sound - Meow!"
    ↑
Dog appends " - Woof!" → "Some sound - Meow! - Woof!"

This diagram is worth studying carefully. The execution flows down through super() calls until it reaches the base, then unwinds back up through each return value. The output is built from the bottom up, not the top down.


Inspecting and Debugging MRO

You have three tools for inspecting MRO, and you should reach for them the moment behavior surprises you.

1. __mro__ attribute: Returns a tuple of classes in resolution order.

python
class A:
    pass
 
class B(A):
    pass
 
class C(A):
    pass
 
class D(B, C):
    pass
 
for cls in D.__mro__:
    print(cls.__name__)
# Output: D, B, C, A, object

2. mro() method: Returns the same information as a list.

python
print(D.mro())
# Output: [<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>]

3. inspect.getmro() (from the inspect module): Identical result, just a different import style that some developers prefer for readability.

python
import inspect
 
print(inspect.getmro(D))
# Output: (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)

All three give you the same information. Use whichever feels most readable in context. The important habit is to actually use them when something is not behaving the way you expect.

Debugging tip: When a method is not called as expected, print the MRO. The answer is always there.

python
class Renderer:
    def render(self):
        return "Default render"
 
class ColorRenderer(Renderer):
    def render(self):
        return super().render() + " + colors"
 
class ThemeRenderer(Renderer):
    def render(self):
        return super().render() + " + theme"
 
class FullRenderer(ColorRenderer, ThemeRenderer):
    pass
 
renderer = FullRenderer()
print(renderer.render())
# Expected: "Default render + colors + theme"
# Actual: ???
 
print(FullRenderer.__mro__)
# Check the order!

Run this and the MRO will immediately tell you whether ColorRenderer or ThemeRenderer is first in line, and whether super() is threading through both of them correctly. The output in this case should be "Default render + theme + colors", ThemeRenderer runs first because ColorRenderer.super() chains to it, then Renderer runs last and builds the base string that gets appended to as the call unwinds.


Mixin Classes: The Right Way to Do Multiple Inheritance

Multiple inheritance is dangerous when used carelessly, but mixin classes make it sane and practical. A mixin is a class designed to be combined with others, providing specific behavior without expecting to stand alone. The defining characteristics of a well-designed mixin are that it addresses exactly one concern, it does not have its own meaningful base class, and it is named to communicate its purpose (often with the word "Mixin" in the name).

Here is the pattern:

python
class LoggingMixin:
    """Mixin: adds logging to any class."""
    def log(self, message):
        print(f"[LOG] {message}")
 
class DatabaseMixin:
    """Mixin: adds database operations to any class."""
    def query(self, sql):
        return f"Executing: {sql}"
 
class BaseRepository:
    """Base class: core repository logic."""
    def fetch(self):
        return "Fetching data..."
 
class UserRepository(LoggingMixin, DatabaseMixin, BaseRepository):
    """Combines logging, database, and base repo."""
    def get_user(self, user_id):
        self.log(f"Getting user {user_id}")
        return self.query(f"SELECT * FROM users WHERE id={user_id}")
 
repo = UserRepository()
print(repo.get_user(42))
# Output: [LOG] Getting user 42
#         Executing: SELECT * FROM users WHERE id=42
 
print(UserRepository.__mro__)
# Output: (UserRepository, LoggingMixin, DatabaseMixin, BaseRepository, object)

The get_user method uses both self.log() from LoggingMixin and self.query() from DatabaseMixin as if they were native methods of UserRepository. They were injected by the inheritance declaration, cleanly and with no conflict because neither mixin defines the same method as the other.

The mixin pattern:

  1. Mixins come first in the inheritance list.
  2. Each mixin is self-contained and does not call super() (usually).
  3. The base class comes last and may call super().
  4. No diamond problems because mixins do not overlap.

This is how real Python libraries handle multiple behavior injection. Django's class-based views use it extensively. Python's standard library uses it in socketserver, http.server, and elsewhere. Once you recognize the pattern, you will see it everywhere.


isinstance() and issubclass(): Type Checking with Inheritance

Once you have got inheritance, you need to check types. Python gives you two built-in functions for this, and both of them are inheritance-aware.

python
class Animal:
    pass
 
class Dog(Animal):
    pass
 
dog = Dog()
 
print(isinstance(dog, Dog))      # True
print(isinstance(dog, Animal))   # True
print(isinstance(dog, str))      # False
 
print(issubclass(Dog, Animal))   # True
print(issubclass(Dog, Dog))      # True
print(issubclass(Animal, Dog))   # False

isinstance() returns True if the object is an instance of the given class or any subclass of it. This is the function you want almost every time, it respects the inheritance hierarchy. issubclass() checks the class hierarchy directly, without needing an instance.

Real-world use: These functions let you write polymorphic code that still handles special cases when needed.

python
def process(entity):
    if isinstance(entity, Dog):
        print("Processing dog...")
    elif isinstance(entity, Animal):
        print("Processing generic animal...")
    else:
        print("Not an animal!")
 
process(Dog())     # Processing dog...
process(Animal())  # Processing generic animal...

The isinstance() check flows down the inheritance tree. A Dog instance matches isinstance(entity, Dog) first, so it prints "Processing dog..." even though it would also match isinstance(entity, Animal). The ordering of your if-elif chain matters when multiple checks could match.


When NOT to Use Inheritance

Knowing when to reach for inheritance is only half the skill. Knowing when to put it down is equally important, and this is where many developers go wrong.

The clearest signal that you should not use inheritance is when the is-a relationship does not hold up under scrutiny. If you find yourself saying "a Stack is-a List" just because you want to reuse List's internal storage, you are about to create a Stack that can do append, insert, remove, pop, sort, and every other List operation, which is not what a stack should do. A stack should only push and pop. The right answer there is composition: give your Stack class a private list internally, and expose only the methods that a stack should have.

The second signal is deep inheritance chains. When you find yourself with D(C), C(B), B(A) going four or five levels deep, and you are not implementing an actual conceptual hierarchy, that is a design smell. Deep chains are hard to follow, hard to debug, and usually a sign that you are using inheritance to share code rather than to express relationships. Flatten them with composition and mixins.

The third signal is when you are inheriting from a class you do not own. If you subclass a third-party library class to add behavior, you are at the mercy of that library's future changes. A method you overrode could have its signature changed, or the parent could add a new method that conflicts with one you added, or internal implementation details you depended on could be refactored. In these cases, the wrapper pattern, where you hold an instance of the external class and delegate to it, is far safer.

The practical rule of thumb: if you can satisfy your need with composition (having an instance of another class) rather than inheritance (being a kind of another class), prefer composition. Inheritance is a strong coupling. Use it when the relationship is real and long-term.


Method Resolution Order in Action: A Real Example

Let us tie it all together with a realistic scenario:

python
class CacheLayer:
    """Mixin: caches results."""
    def __init__(self):
        self.cache = {}
 
    def get_cached(self, key):
        return self.cache.get(key)
 
    def set_cached(self, key, value):
        self.cache[key] = value
 
class TimestampMixin:
    """Mixin: adds timestamps."""
    def get_timestamp(self):
        from datetime import datetime
        return datetime.now().isoformat()
 
class Database:
    """Base class: database connection."""
    def __init__(self, host, port):
        self.host = host
        self.port = port
 
    def connect(self):
        return f"Connected to {self.host}:{self.port}"
 
class CachedTimestampDatabase(CacheLayer, TimestampMixin, Database):
    """Combine caching, timestamps, and database."""
    def __init__(self, host, port):
        CacheLayer.__init__(self)
        Database.__init__(self, host, port)
 
    def query(self, sql):
        cached = self.get_cached(sql)
        if cached:
            return f"[CACHED] {cached}"
 
        result = f"Query result for: {sql}"
        self.set_cached(sql, result)
        return f"[{self.get_timestamp()}] {result}"
 
db = CachedTimestampDatabase("localhost", 5432)
print(db.connect())
# Output: Connected to localhost:5432
 
print(db.query("SELECT * FROM users"))
# Output: [2025-02-25T14:30:45.123456] Query result for: SELECT * FROM users
 
print(db.query("SELECT * FROM users"))
# Output: [CACHED] Query result for: SELECT * FROM users
 
print(CachedTimestampDatabase.__mro__)
# Output: (CachedTimestampDatabase, CacheLayer, TimestampMixin, Database, object)

The second call to query() returns the cached result immediately, saving the timestamp overhead. The first call stamps and stores. This is a real, useful pattern used in production systems, cache-aside logic layered onto a database object without modifying the database class at all.

Notice: we manually called both CacheLayer.__init__() and Database.__init__() because they do not call super(). If they did, we could simplify:

python
class CacheLayer:
    def __init__(self):
        self.cache = {}
        super().__init__()  # Cooperatively call next
 
class Database:
    def __init__(self, host, port):
        self.host = host
        self.port = port
        # Don't call super() (object.__init__ doesn't need args)
 
class CachedTimestampDatabase(CacheLayer, Database):
    def __init__(self, host, port):
        CacheLayer.__init__(self, host, port)  # Still manual for args

Actually, this gets messy with arguments. When constructors take different arguments, cooperative super() calls become awkward because each class in the chain needs to pass the right arguments along. The mixin pattern, mixin first, no super() in the mixin constructor, is cleaner and avoids the argument-passing puzzle.


Common Pitfalls and How to Avoid Them

Pitfall 1: Forgetting that super() Doesn't Mean "Parent"

It means "next in MRO," which might not be your direct parent in multiple inheritance.

Fix: Always check __mro__ when behavior surprises you.

Pitfall 2: Hardcoding Parent Class Names

python
class Child(Parent):
    def method(self):
        Parent.method(self)  # Brittle!

This works when the class hierarchy is static and you own everything, but it breaks cooperative multiple inheritance and is hostile to subclassing.

Fix: Use super() instead.

Pitfall 3: Diamond Inheritance Without Cooperation

python
class A:
    def method(self):
        print("A")
 
class B(A):
    def method(self):
        A.method(self)  # Skips C!
 
class C(A):
    def method(self):
        A.method(self)
 
class D(B, C):
    pass
 
D().method()  # Only prints "A", C is skipped

When B calls A.method(self) directly instead of using super(), it bypasses the MRO entirely. C.method() never runs, even though the MRO would have routed through it.

Fix: Use super() and ensure all classes cooperate.

Pitfall 4: Too Much Multiple Inheritance

If your MRO is more than four or five classes deep, you have probably over-complicated things. Deep MROs are hard to debug, hard to reason about, and often indicate that the abstraction boundaries are wrong.

Fix: Use composition or mixins more carefully. Prefer single inheritance when possible. When in doubt, flatten.


Key Takeaways

  • Single inheritance is straightforward: extend a parent, override methods, use super() for cooperative calls.
  • super() calls the next class in the MRO, not literally the parent. This enables cooperative multiple inheritance.
  • MRO is computed via C3 linearization, an algorithm that respects inheritance order while avoiding duplicates and ensuring each class appears exactly once.
  • Inspect with __mro__ whenever behavior surprises you. The answer is always in that list.
  • The diamond problem is solved by super() ensuring every class in the chain gets a turn.
  • Mixin classes are the practical way to do multiple inheritance: put them first, keep them self-contained, name them clearly.
  • isinstance() and issubclass() navigate the inheritance tree to check types in an inheritance-aware way.
  • Prefer composition over inheritance when the is-a relationship is not genuine, having is often better than being.

Master these concepts, and you will write classes that are flexible, reusable, and do not break when subclassed. The MRO is not a gotcha to avoid; it is a tool to understand and use deliberately.

Need help implementing this?

We build automation systems like this for clients every day.

Discuss Your Project