Back to Blog
Python

Python Multiple Inheritance: MRO and super() Explained

python multiple inheritance: Understand Python multiple inheritance: MRO, super() calls, diamond problem, and practical patterns for maintainable class design.

multiple inheritancemethod resolution ordersuper()diamond inheritancemixinscomposition
Diagram showing multiple inheritance class hierarchy with method resolution order arrows in Python

When a class inherits from more than one parent, Python must decide which method to call when the same name appears in multiple parents. That decision is made by the method resolution order (MRO), and getting it wrong leads to confusing AttributeError or unexpected behavior. This article explains how python multiple inheritance works, how to use super() correctly, and when to avoid multiple inheritance altogether.

How Python Resolves Methods Across Multiple Parents

Python builds a linear order for every class, known as the MRO. This order determines which attribute or method is looked up first when you access it on an instance. The MRO is not simply the order of bases as written; it follows a specific algorithm that preserves local precedence and monotonicity.

Consider a simple example:

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

When you call D().greet(), Python uses the MRO of D. You can inspect it with D.__mro__:

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

The lookup starts at D, then B, then C, then A, and finally object. So D().greet() returns "Hello from B" because B appears before C in the base list. If B did not define greet, the lookup would continue to C.

This linearization is not arbitrary. It ensures that a class always appears before its parents, and that the order of bases is respected when possible. The algorithm that produces this order is the C3 linearization.

The C3 Linearization Algorithm Behind MRO

C3 is the algorithm Python uses to compute the MRO. It merges the linearizations of the parent classes and the parents themselves, following a set of rules that guarantee consistency. The key constraints are:

  • Each class appears exactly once in the MRO.
  • A class always appears before its parents.
  • If two classes share a parent, the order in which they appear in the base list of the child determines which one is visited first.

You do not need to implement C3 yourself, but understanding its output helps you predict method resolution. The mro() method on a class returns the same list as __mro__.

A common misconception is that the MRO is simply depth-first left-to-right. That would produce D, B, A, C, A for the diamond example, which is invalid because A would appear twice. C3 removes duplicates while preserving the relative order of each parent's own MRO.

For the diamond pattern where B and C both inherit from A, the MRO of D(B, C) is D, B, C, A, object. This means A is visited after both B and C, which is the behavior you want when B and C override methods from A.

Using super() in a Multiple Inheritance Hierarchy

super() is a built-in that returns a proxy object that delegates method calls to the next class in the MRO. It is not a reference to the parent class in the traditional sense; it follows the MRO dynamically. This is especially important in multiple inheritance, where the next class may not be the direct parent you wrote in the class definition.

Consider this example:

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

When you instantiate D(), the output is:

D.__init__
B.__init__
C.__init__
A.__init__

Notice that C.__init__ is called even though C is not a direct parent of B. This happens because super() in B looks at the MRO of D and finds C as the next class. This cooperative behavior is what makes multiple inheritance work cleanly when every class uses super() consistently.

The critical rule is that every class in the hierarchy must call super() in the same method (like __init__) and must accept the same arguments, or at least use *args, **kwargs to pass them along. If one class skips the super() call, the chain breaks and subsequent classes never run.

The Diamond Problem and How Python Handles It

The diamond problem occurs when a class inherits from two classes that share a common ancestor. In languages without a well-defined MRO, this can cause ambiguity. Python resolves it with C3, ensuring that the common ancestor is only called once in the MRO.

Using the earlier diamond example:

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

Calling D().method() produces:

D.method
B.method
C.method
A.method

A.method runs exactly once. If A.method had side effects like opening a file or acquiring a lock, you would not want it to run twice. Python's MRO guarantees that each class in the hierarchy is visited once per method call.

However, the diamond pattern can still cause subtle issues if some classes do not cooperate. For example, if B calls super().method() but C does not, then A.method will never run. The MRO still exists, but the chain is broken. This is why cooperative multiple inheritance requires discipline: every class that overrides a method should call super() unless it intentionally wants to stop the chain.

Mixins: A Practical Use of Multiple Inheritance

Mixins are small classes that provide a specific set of behaviors or capabilities, designed to be combined with other classes. They are not meant to stand alone; they rely on the host class to provide certain methods or attributes. This is a common and effective use of python multiple inheritance.

For example, you might have a mixin that adds JSON serialization:

class JsonMixin: def to_json(self): import json return json.dumps(self.__dict__) class User: def __init__(self, name, email): self.name = name self.email = email class AdminUser(User, JsonMixin): pass

Now AdminUser instances have both the User behavior and the to_json method. The MRO for AdminUser is AdminUser, User, JsonMixin, object. If User also defined to_json, the order would matter. Mixins are usually placed after the primary base class to avoid overriding methods unintentionally.

Mixins work well when they are narrow and do not require complex constructor arguments. They often rely on duck typing, so the host class must provide the expected attributes. This pattern keeps code DRY and allows you to compose behaviors without deep inheritance chains.

When to Prefer Composition Over Multiple Inheritance

Multiple inheritance is powerful, but it adds complexity. The MRO can become hard to reason about when the hierarchy grows beyond a few levels. If you find yourself writing classes with many bases and frequent overrides, composition may be a better choice.

Composition means holding an instance of another class as an attribute and delegating to it. For example, instead of inheriting from LoggerMixin, you could give your class a logger attribute and call its methods. This avoids the coupling that inheritance introduces and makes dependencies explicit.

A practical rule is to use multiple inheritance when you have a clear set of independent mixins that do not overlap in method names. If two mixins define the same method, the MRO will pick one, and you may not get the combined behavior you expect. In that case, composition or a different design is safer.

Another consideration is the constructor. When multiple classes in the hierarchy require __init__ arguments, you must coordinate them through super() calls. This becomes brittle if the set of arguments changes. Composition lets each component manage its own initialization independently.

For example, a class that needs both logging and database access can be written with composition:

class Service: def __init__(self, logger, repository): self.logger = logger self.repository = repository def fetch(self, key): self.logger.info(f"Fetching {key}") return self.repository.get(key)

This is easier to test and extend than a class that inherits from LoggerMixin and RepositoryMixin. The tradeoff is that you write more delegation code, but the relationships are explicit and the MRO is not involved.

Inspecting MRO and Debugging Inheritance Conflicts

When you encounter unexpected behavior in a multiple inheritance hierarchy, the first step is to inspect the MRO. You can print ClassName.__mro__ or use ClassName.mro(). This shows the exact order in which Python will search for attributes.

If a method call does not do what you expect, check whether the class that should handle it appears in the MRO before another class that defines the same method. For example:

class X: def foo(self): return "X" class Y: def foo(self): return "Y" class Z(X, Y): pass print(Z().foo()) # "X"

If you want Y.foo to be called, you would need to reverse the bases: class Z(Y, X). Changing the base order is the simplest way to alter the MRO, but it can have ripple effects on other methods.

Another debugging technique is to add temporary print statements in each method to trace the chain of super() calls. This is especially useful when you suspect a class is not being reached. Remember that super() does not always call the immediate parent; it calls the next class in the MRO, which may be a sibling.

Python also provides the __mro_entries__ hook for customizing MRO behavior in metaclasses, but that is an advanced topic rarely needed in application code. For most developers, understanding the MRO and using super() cooperatively is enough to handle multiple inheritance safely.

One final operational concern is maintainability. Even if the MRO works today, a future change to one of the base classes can alter the resolution order in surprising ways. Adding a new base class or changing the order of bases can break the cooperative super() chain. This is why many Python codebases prefer composition or a single inheritance hierarchy with mixins that are carefully designed to avoid method name collisions. If you do use multiple inheritance, document the intended MRO and keep the hierarchy shallow.

python multiple inheritance: Practical Usage and Code Exampl | RYUSLOG DEV