Python: Multiple Inheritance & MRO

Python multiple inheritance lets one class inherit from several parents at once. This guide explains the diamond problem, Method Resolution Order (MRO), C3 linearization, and how Python decides which method actually runs.

“Everyone knows that debugging is twice as hard as writing a program in the first place.”

Brian Kernighan, The Elements of Programming Style

Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Intermediate | Reading Time: 13 minutes

Prerequisites: Inheritance

Here is the question that breaks people. A class inherits from two parents. Both parents have a method with the same name. So which one runs? That puzzle is called the diamond problem, and it has tripped up developers in every language that allows multiple inheritance. Python does not leave it to chance. It uses a fixed, predictable algorithm called C3 linearization to build a single search order, and then it follows that order every single time.

Think of it like a relay race: the baton (your method call) gets passed down a lane that was decided before the race even started. Once you can read that lane, multiple inheritance stops feeling like guesswork. Let us see how it actually works.

Python Multiple Inheritance: The Basics

Final MROD.__mro__ =(D, B, C, A, object)D.method() searches:D first, then B, then C,then A, then objectC3 Linearization StepsStep 1: Start with DStep 2: Check B first(listed first in D’s parents)Step 3: Check C next(listed second)Step 4: Check A(parent of both B and C)Step 5: object(root of everything)Diamond InheritanceA (base)B(A)C(A)D(B, C)Python MRO: How C3 Linearization Resolves the Diamond to D, B, C, A, object

Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.

The diagram shows Python multiple inheritance and the C3 linearization algorithm solving the diamond problem. When class D inherits from both B and C, and both of those inherit from A, Python works out the MRO as D, B, C, A, object. This order is fixed and predictable. Every class shows up exactly once, and a child is always checked before its parents. This matters because super() walks this exact list, not just the one parent directly above.

📄 multiple_basic.py: inheriting from two parents

class Flyable:
    def fly(self):
        return f"{self.name} is flying!"

class Swimmable:
    def swim(self):
        return f"{self.name} is swimming!"

class Duck(Flyable, Swimmable):
    def __init__(self, name):
        self.name = name

    def quack(self):
        return f"{self.name} says: Quack!"

donald = Duck("Donald")
print(donald.fly())    # From Flyable
print(donald.swim())   # From Swimmable
print(donald.quack())  # Duck's own
print(f"MRO: {[c.__name__ for c in Duck.__mro__]}")

▶ Output

Donald is flying!
Donald is swimming!
Donald says: Quack!
MRO: ['Duck', 'Flyable', 'Swimmable', 'object']

What happened here: Duck(Flyable, Swimmable) inherits from both parents in one go. Duck never defines fly() or swim(), yet it can call both, because it borrowed them from its parents. Think of Duck as a kid who picked up cooking from one parent and swimming from the other: two separate skills, one child, nothing learned from scratch. The MRO line at the end is the search order Python will use: Duck first, then Flyable (it was listed first in the declaration), then Swimmable, then object, the base of every Python class.

The Diamond Problem

A quick real-life picture first. Imagine a kid named Anvay who learns the same dal recipe from both his mother and his grandmother. The two versions differ slightly, so when Anvay cooks, whose recipe does he follow? He needs one fixed rule to decide. Classes hit the exact same dilemma when two parents share a grandparent and define the same method, and Python’s answer is also a fixed rule.

📄 diamond.py: when two parents share a grandparent

class A:
    def greet(self):
        return "Hello from A"

class B(A):
    def greet(self):
        return "Hello from B"

class C(A):
    def greet(self):
        return "Hello from C"

class D(B, C):
    pass  # Does not override greet, so who wins?

d = D()
print(d.greet())
print(f"MRO: {[cls.__name__ for cls in D.__mro__]}")

▶ Output

Hello from B
MRO: ['D', 'B', 'C', 'A', 'object']

What happened here: D inherits from both B and C, and both of them inherit from A. Draw that out and the shape looks like a diamond, which is where the name comes from. When you call d.greet(), Python walks the MRO in order: it checks D first (no greet there), then B (B has one), and it stops the moment it finds a match. B was listed first in D(B, C), so B wins. Flip the order to D(C, B) and C would win instead. The order you write the parents in is the order Python searches them, so it genuinely matters.

C3 Linearization: How MRO Actually Works

So how does Python build that search order in the first place? It runs an algorithm called C3 linearization, which has been the default since Python 2.3. Think of it like a seating chart for a wedding: worked out once before the event, and after that everyone just checks the chart instead of arguing at the door. You do not have to compute it by hand (Python does it for you and caches the result), but knowing the rules turns MRO from a mystery into something you can predict. The rules are:

  1. The class itself comes first
  2. Parents are ordered left-to-right as declared
  3. A parent cannot appear before its children
  4. If two classes are in the same level, preserve the declaration order

📄 c3_trace.py: tracing MRO step by step

class A:
    def method(self):
        print("A.method")

class B(A):
    def method(self):
        print("B.method")
        super().method()  # Next in MRO, not "parent"

class C(A):
    def method(self):
        print("C.method")
        super().method()

class D(B, C):
    def method(self):
        print("D.method")
        super().method()

print("Calling D().method():")
D().method()
print(f"\nMRO: {' -> '.join(c.__name__ for c in D.__mro__)}")

▶ Output

Calling D().method():
D.method
B.method
C.method
A.method

MRO: D -> B -> C -> A -> object

What happened here: This is the part that surprises everyone, so read it slowly. super() does not mean “call my parent.” It means “call the next class in the MRO.” From D, super() jumps to B. From B, super() jumps to C, not to A, even though A is B’s actual parent. From C it finally reaches A. Because the MRO lists each class once, every method in the diamond runs exactly one time, in a clean top-to-bottom pass.

That is why you reach for super() here instead of writing the parent name by hand. If B had called A.method(self) directly, A would run twice (once from B’s branch and once from C’s branch) and that double call is exactly the bug C3 was designed to prevent.

Cooperative super() with **kwargs

Real hierarchies need __init__ arguments, and that is exactly where multiple inheritance usually bites. The fix is a pattern where every class keeps the argument it needs and passes the rest along. In the example below, a new user named Niranjan signs up, and his details flow cleanly through three classes without a single hardcoded parent call.

📄 cooperative_super.py: the pattern for safe multiple inheritance

class Base:
    def __init__(self, **kwargs):
        # Absorb any leftover kwargs, the chain ends here
        pass

class Loggable(Base):
    def __init__(self, log_level="INFO", **kwargs):
        super().__init__(**kwargs)
        self.log_level = log_level

    def log(self, message):
        print(f"[{self.log_level}] {message}")

class Serializable(Base):
    def __init__(self, format="json", **kwargs):
        super().__init__(**kwargs)
        self.format = format

    def serialize(self):
        import json
        return json.dumps(self.__dict__)

class User(Loggable, Serializable):
    def __init__(self, name, age, **kwargs):
        super().__init__(**kwargs)
        self.name = name
        self.age = age

# Pass all kwargs, they flow through the MRO
user = User("Niranjan", 27, log_level="DEBUG", format="json")
user.log(f"Created user: {user.name}")
print(user.serialize())

▶ Output

[DEBUG] Created user: Niranjan
{"format": "json", "log_level": "DEBUG", "name": "Niranjan", "age": 27}

What happened here: This is the cooperative multiple inheritance pattern, and it is the safe way to write __init__ across many parents. Every class accepts **kwargs and passes the leftovers up the chain with super().__init__(**kwargs). Each class grabs only the keyword it cares about (log_level for Loggable, format for Serializable) and forwards the rest. The Base class sits at the end and quietly swallows whatever is left. Picture a relay where each runner keeps the one baton meant for them and hands the rest forward. This is what stops the dreaded TypeError: __init__() got an unexpected keyword argument crash.

One detail worth noticing in the output: the serialized dictionary lists format before log_level, even though we passed log_level first. That is the MRO in action. User.__init__ calls super().__init__, which lands on Loggable. Loggable immediately calls super().__init__ before setting its own attribute, so control flows down to Serializable, which sets format first. Then the calls unwind back upward, setting log_level, then name, then age. Attributes are stored in the order they are actually assigned, not the order you typed the arguments.

Mixins: The Practical Pattern

Say you run a small blog. An author named Rahul publishes posts, and a reader named Aditi leaves comments. Posts and comments are different things, yet both need a creation timestamp and a readable printout for debugging. Mixins let you write each of those features exactly once and attach them to both classes. This is Python multiple inheritance at its most practical.

📄 mixins.py: small, focused classes mixed into others

class TimestampMixin:
    def created_at(self):
        from datetime import datetime
        return datetime.now().strftime("%Y-%m-%d %H:%M")

class RepresentationMixin:
    def __repr__(self):
        attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
        return f"{self.__class__.__name__}({attrs})"

class BlogPost(TimestampMixin, RepresentationMixin):
    def __init__(self, title, author):
        self.title = title
        self.author = author

class Comment(TimestampMixin, RepresentationMixin):
    def __init__(self, text, user):
        self.text = text
        self.user = user

post = BlogPost("Python OOP Guide", "Rahul")
comment = Comment("Great article!", "Aditi")

print(post)
print(comment)
print(f"Post created: {post.created_at()}")

▶ Output

BlogPost(title='Python OOP Guide', author='Rahul')
Comment(text='Great article!', user='Aditi')
Post created: 2026-06-21 15:10

What happened here: Mixins are tiny, single-purpose classes that you blend into a bigger class to add one capability. TimestampMixin contributes a timestamp method. RepresentationMixin contributes a nice __repr__. Neither one makes sense as a standalone object, and that is the point. They are like seasonings, useless as a meal on their own, but you sprinkle them onto BlogPost and Comment to give each class a feature without copying code. Your timestamp will read the current date and time, so it will not match mine exactly. This mix-and-match style is by far the most common and most practical use of multiple inheritance in real Python code.

Common Mistakes

Mistake 1: Not using super() in multiple inheritance

Hardcoding ParentClass.__init__(self) breaks the MRO chain and can run the same grandparent twice. Always reach for super() instead. It follows the MRO and makes sure each class in the hierarchy runs exactly once, no more and no less.

Mistake 2: Conflicting method names without explicit override

If two parents define the same method, the MRO quietly picks one and you might not notice the other was skipped. When you genuinely need both, override the method in the child and call each parent explicitly: Parent1.method(self) and Parent2.method(self). That way the choice is visible in your code instead of hidden in the MRO.

Best Practices

  • DO use super() consistently throughout the hierarchy
  • DO use **kwargs for cooperative multiple inheritance
  • DO prefer mixins (small, focused classes) over deep multiple inheritance
  • DO check ClassName.__mro__ when debugging method resolution
  • DON’T hardcode parent names, use super() instead
  • DON’T build deep, tangled diamond hierarchies unless you truly have no simpler option

Conclusion

Python multiple inheritance is powerful, but it only feels safe once you can read the MRO. Python’s C3 linearization gives you a fixed, predictable search order, so method resolution is never a guessing game. Cooperative super() with **kwargs tames the diamond problem, and mixins hand you the day-to-day benefits of multiple inheritance without the tangle. Whenever a hierarchy confuses you, print ClassName.__mro__ and read the list. The answer is always right there.

Inheritance gives you shared behavior, while polymorphism gives you flexible behavior. In the polymorphism tutorial, you will see how the same method name can produce completely different results depending on which object calls it, and why Python’s duck typing makes that idea even more powerful. And if you want to jump around, every chapter from the basics to AI/ML is listed on the Python + AI/ML tutorial series home.

Practice Exercises

  1. Predict the MRO: Define classes A, B(A), C(A), and D(C, B). Before running anything, write down what you think D.__mro__ will be. Then print it and check whether the swapped parent order (C, B instead of B, C) changed the result the way you expected.
  2. Trace the super() chain: Take the c3_trace.py example and add a print(f"entering {type(self).__name__}") at the top of each method. Run it and confirm by eye that every class runs exactly once, in MRO order.
  3. Build your own mixin: Write a JsonMixin that adds a to_json() method and a ComparableMixin that adds __eq__ based on __dict__. Mix both into a small Product class (name, price) and prove that two products with the same data compare equal.

Frequently Asked Questions

What is the diamond problem in Python?

The diamond problem occurs when a class inherits from two classes that share a common ancestor. Without MRO, the ancestor’s methods could be called multiple times or the wrong version could be selected. Python’s C3 linearization solves this by defining a single, deterministic method lookup order.

What is MRO in Python?

MRO (Method Resolution Order) is the order Python follows when looking up methods in a class hierarchy. Check it with ClassName.__mro__ or ClassName.mro(). Python uses C3 linearization to compute MRO, ensuring each class appears exactly once.

Does super() always call the parent class?

No. super() calls the next class in the MRO, which may not be the direct parent. In a diamond with D(B, C) where B and C both inherit from A, super() in B goes to C, not A. This ensures every class in the hierarchy is initialized exactly once.

What are Python mixins?

Mixins are small, focused classes designed to add specific functionality when combined with other classes via multiple inheritance. They typically don’t have __init__ and add one or two methods. Examples: LoggingMixin, SerializableMixin, TimestampMixin.

When should I use multiple inheritance in Python?

Use Python multiple inheritance for mixins (adding small, orthogonal capabilities) and interface-like patterns. Avoid it for modeling complex ‘is-a’ relationships with overlapping functionality. If your inheritance diagram looks like a spider web, switch to composition.

Interview Questions on Multiple Inheritance

The same ideas as they show up in real interviews, framed as scenarios you can practice out loud.

Q: You define class D(A, B) and Python immediately raises TypeError: Cannot create a consistent method resolution order (MRO). What causes this and how do you fix it?

C3 linearization failed because your base class order contradicts an existing order. The classic trigger is listing a class before its own subclass, for example class D(A, B) where B already inherits from A: C3 requires B (the child) to come before A, but your declaration demands the opposite, so no consistent order exists. Fix it by reordering the bases to D(B, A), or step back and simplify the hierarchy. The error at class definition time is a feature: Python refuses to guess.

Q: A teammate replaced super().__init__() with direct calls like Loggable.__init__(self) in a diamond hierarchy, and now a shared base class initializes twice and resets state. What went wrong?

Hardcoded parent calls bypass the MRO, so each branch of the diamond reaches the shared ancestor separately and its __init__ runs once per branch. super() instead walks the linearized MRO, where every class appears exactly once, so the ancestor initializes a single time. The fix is to make every class in the hierarchy call super().__init__(**kwargs) cooperatively. Partial cooperation is not enough: one hardcoded call anywhere breaks the chain for everyone.

Q: What guarantees does C3 linearization give you about the MRO?

Three things: every class appears exactly once in the list, a subclass always appears before its superclasses, and the left-to-right order of declared parents is preserved. If those constraints cannot all be satisfied at once, Python raises a TypeError when the class is defined rather than picking an arbitrary order. That determinism is what makes super() chains safe to reason about.

Q: Why do cooperative __init__ methods accept **kwargs, and what breaks without them?

Each class in the MRO consumes only the keyword arguments it understands and forwards the rest with super().__init__(**kwargs). Without **kwargs, any argument meant for a class later in the chain hits an intermediate __init__ that does not recognize it, and you get TypeError: __init__() got an unexpected keyword argument. The pattern makes every class agnostic about who sits after it in the MRO, which is the whole point of cooperative inheritance.

Q: Where should a mixin go in the base class list, and does the position actually matter?

Mixins conventionally go to the left of the main base class, for example class ApiView(JsonMixin, BaseView). Position matters because the base list order is the MRO search order: putting the mixin first means its methods win lookups and its super() calls forward into the main class. Put it last and the main class may shadow the mixin’s methods, silently disabling the behavior you mixed in.

Q: What does zero-argument super() actually do in Python 3?

Inside a method, super() is shorthand for super(CurrentClass, self): the compiler fills in the class the method was defined in. It returns a proxy object that continues attribute lookup from the class immediately after CurrentClass in type(self).__mro__. That is why the same super().method() line in class B can land on different classes depending on which subclass the instance belongs to.

Further reading: for the full reference, see the official Python documentation.

Previous: Python Inheritance: Single, Multi-level, Hierarchical

Next: Python: Polymorphism, Method Overriding & Duck Typing

Series Home: Python + AI/ML Tutorial Series

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *