Back to Blog
Python

Python Multilevel Inheritance: Syntax, MRO, and Pitfalls

python multilevel inheritance: Understand Python multilevel inheritance: how attribute lookup works, using super() correctly, and avoiding common design pitfalls.

PythoninheritanceMROsuper()class design
Diagram showing a three-level Python class hierarchy with method resolution order arrows.

python multilevel inheritance requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

A Minimal Multilevel Inheritance Example

In Python, multilevel inheritance means a class inherits from a parent, which itself inherits from another class. The chain can extend arbitrarily deep. A simple three-level hierarchy looks like this:

class Base: def greet(self): return "Hello from Base" class Intermediate(Base): def greet(self): return "Hello from Intermediate" class Derived(Intermediate): pass

When you call greet() on a Derived instance, Python looks for the method in Derived, then Intermediate, then Base. Because Derived does not override greet, the version from Intermediate is used. This lookup path is defined by the method resolution order (MRO), which is computed for every class when it is created.

How Attribute Lookup Follows the MRO

Every class in Python has an __mro__ attribute that lists the classes in the order Python searches for attributes and methods. For Derived, the MRO is:

print(Derived.__mro__) # (<class '__main__.Derived'>, <class '__main__.Intermediate'>, <class '__main__.Base'>, <class 'object'>)

The search starts at the class itself and moves up the chain. This linearization is deterministic and follows the C3 linearization algorithm, which Python uses to handle multiple inheritance as well. For pure single-inheritance chains, the MRO is simply the chain from the class to object.

Using super() in a Multilevel Hierarchy

super() returns a proxy object that delegates method calls to the next class in the MRO. This is crucial when each level in the hierarchy needs to extend behavior from its parent. For example:

class Base: def __init__(self, name): self.name = name class Intermediate(Base): def __init__(self, name, age): super().__init__(name) self.age = age class Derived(Intermediate): def __init__(self, name, age, email): super().__init__(name, age) self.email = email

Each __init__ calls super().__init__() to initialize the parent part. This pattern works because super() in Derived resolves to Intermediate, and super() in Intermediate resolves to Base. If any class in the chain forgets to call super(), the initialization of the base class may be skipped, leading to missing attributes.

Handling the Diamond Problem

Multilevel inheritance can create a diamond shape when combined with multiple inheritance. For instance:

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

Here D inherits from both B and C, which both inherit from A. The MRO for D is [D, B, C, A, object]. When you call d.method(), it prints B, C, A in that order. This cooperative behavior relies on every class in the chain using super() consistently. If one class skips super(), the chain breaks and later classes never run.

Common Mistakes with Multilevel Inheritance

A frequent error is forgetting to call super().__init__() in an intermediate class. Another is overriding a method but not calling the parent version when it is required. Name conflicts can also arise when two levels define the same attribute with different meanings. These issues are not syntax errors; they surface as missing attributes or unexpected behavior at runtime, which makes them harder to debug.

Another subtlety is that super() does not always refer to the direct parent. It refers to the next class in the MRO, which can be surprising when the hierarchy changes after refactoring. Always inspect __mro__ when the behavior of a method call is not what you expect.

When to Prefer Composition Over Deep Hierarchies

Deep multilevel inheritance chains become difficult to maintain. A change in a base class can affect every descendant, and the flow of initialization can be hard to trace. Composition—where a class holds an instance of another class instead of inheriting from it—often provides a clearer relationship and reduces coupling. For example, instead of Car inheriting from Vehicle which inherits from Machine, you might give Car an Engine and a Transmission. The rule of thumb is to use inheritance when there is a genuine "is-a" relationship and the hierarchy is shallow. If the chain exceeds three levels or you find yourself overriding many methods just to adjust behavior, composition is usually a better choice.

Inspecting the MRO at Runtime

You can always inspect the MRO of a class at runtime to understand how method calls will resolve. The __mro__ attribute is a tuple of classes, and you can also use Class.mro() as a method. This is particularly useful when debugging a complex hierarchy or verifying that a mixin is placed correctly. For example:

print(Derived.mro())

This output helps you confirm the order in which super() calls will propagate. It also reveals the exact position of object, which is always last.

python multilevel inheritance: Practical Usage and Code Exam | RYUSLOG DEV