Python Method Resolution Order

Python supports multiple inheritance. That is, a child class can inherit from more than one parent. But, what happens if a method is implemented in more than one ancestor? Which of the methods available will be called?

If you already have a pretty good grasp on Python’s MRO, test yourself with the advanced examples.

A basic example

If you have crossed paths with multiple inheritance in the past, you may know that if you have a child class with two parent classes, and make a call in the child class to a method that is implemented in both parents, Python’s Method Resolution Order (MRO) will look for it in the parents, from left to right as specified in the child class definition. And, in particular, only the first implementation found following this order will be executed.

Such is the case of the following snippet of code:

class PrivateCar:
    def driven_for(self):
        print("Driven for personal purposes.")

class Taxi:
    def driven_for(self):
        print("Driven for business purposes.")

class RentalCar(PrivateCar, Taxi):
    def rental_car_driven_for(self):
        self.driven_for()
        print("A rental car company owns the car.")

if __name__ == "__main__":
    rental_car = RentalCar()
    rental_car.rental_car_driven_for()

If we execute this snippet of code, “Driven for personal purposes. A rental car company owns the car.” will be printed, as the MRO sees that driven_for is not implemented in RentalCar, it looks for it in PrivateCar, finds it, and uses it. If the method hadn’t been implemented in PrivateCar, then it would have looked for it in Taxi. Easy peasy.

Not so quick, my friend

Let’s extend slightly the code snippet above and introduce a Vehicle base class and, since normally cars are driven for personal purposes, we will move the implementation of driven_for from PrivateCar to this Vehicle class. To still have the same behaviour, PrivateCar will now inherit from Vehicle.

Additionally, let’s add an argument to the driven_for method from Taxi to include the color of the taxi. What do you expect this code to print?

class Vehicle:
    def driven_for(self):
        print("Driven for personal purposes.")

class PrivateCar(Vehicle):
    pass

class Taxi:
    def driven_for(self, color):
        print("Driven for business purposes")
        print(f"They are normally {color}.")

class RentalCar(PrivateCar, Taxi):
    def rental_car_driven_for(self):
        self.driven_for()
        print("A rental car company owns the car.")

if __name__ == "__main__":
    rental_car = RentalCar()
    rental_car.rental_car_driven_for()

What your intution may tell you

Following from the base example, you may expect left-to-right MRO. That is, the method driven_for is not implemented in RentalCar, so we check in PrivateCar, who does not implement it, so we check in its parent, Vehicle, find it and use it. Therefore, this snippet of code should still print: “Driven for personal purposes. A rental car company owns the car.”.

Executing the code will show you exactly that and, therefore, you may be lead to think that the MRO follows a depth-first search approach, which, unfortunately, is not true.

A counter-example to depth-first search (The Diamond Inheritance problem)

If we are honest to ourselves, Taxis are indeed vehicles, so they should also inherit from the Vehicle class, so we will add a connection between the two. That is, our code will look as shown below and we will not expect any changes in behaviour in our code.

class Vehicle:
    def driven_for(self):
        print("Driven for personal purposes.")

class PrivateCar(Vehicle):
    pass

class Taxi(Vehicle):
    def driven_for(self, color):
        print("Driven for business purposes")
        print(f"They are normally {color}.")

class RentalCar(PrivateCar, Taxi):
    def rental_car_driven_for(self):
        self.driven_for()
        print("A rental car company owns the car.")

if __name__ == "__main__":
    rental_car = RentalCar()
    rental_car.rental_car_driven_for()

What see when you execute the code

Traceback (most recent call last):
  File "/home/hector/diamond_example.py",
    line 19, in <module>
    rental_car.rental_car_driven_for()
  File "/home/hector/diamond_example.py",
    line 14, in rental_car_driven_for
    self.driven_for()
TypeError: Taxi.driven_for() missing 1
           required positional argument: 'color'

What!? I thought MRO was depth-first search. Why does it then visit the Taxi method instead of behaving as before!? This looks like breadth-first search! But I thought… :(

The explanation

MRO does neither follow breadth-first nor depth-first ordering. Instead, it does something different. It is not so easy to explain with words, so let’s try to understand the examples here.

Python’s MRO will not visit the grandparent class (Vehicle in this case) until all its children have been checked. That is, in the first example, we follow the order RentalCar -> PrivateCar -> Vehicle because PrivateCar is the only child of Vehicle. However, in the second example, the MRO follows RentalCar -> PrivateCar -> Taxi -> Vehicle, because it needs to visit both PrivateCar and Taxi before visiting Car.

In other words, after visiting a parent, we will need to ask ourselves whether we have visited all the children of any of its parents. If so, we will visit it. If not, we will continue with the next parent. The children considered, however, will be only those in the ascendent path from the root class. That is, if the Vehicle class also had a Bycicle child (which our RentalCar does not inherit from), this class would not be visited.

A formal definition of the algorithm followed by Python’s MRO can be found here. However, if you find this algorithm too confusing or simply do not want to spend time reading it, you can always call a class’ method mro and that will return the order of resolution. For example, if print what RentalCar.mro() returns in the last example, we will get:

[<class '__main__.RentalCar'>, <class '__main__.PrivateCar'>,
<class '__main__.Taxi'>, <class '__main__.Vehicle'>,
<class 'object'>]

Note that every class without any explicit parent, always inherits of the base class ‘object’.

Now, if you think you’re ready, test your understanding in the advanced examples that I have added below. Good luck!

Advanced examples

Example #1

What MRO will be followed in this case?

Solution: E -> B -> C -> D -> A

Explanation: If you have understood the basic case, adding another parent class should not make it any more complex. Following the rule of visiting all children before visiting the parent, tells us to visit B, C and D before finally visiting A.

Example #2

What MRO will be followed in this case?

Solution: F -> C -> D -> A -> E -> B

Explanation: Having parents with different grandparents makes it more complex. First, we start visiting the parents with C. Then we ask ourselves, have we visited all the children of C’s parent (A)? No, we haven’t. So we continue with the parents of F, and visit D. We ask ourselves the same question again. In this case, all of A’s children have been visited so we visit A. Then, we continue with F’s parents, E, and then finally B.

Example #3

What MRO will be followed in this case?

Solution: F -> D -> A -> E -> B -> C

Explanation: We start visiting F’s parents with D. Then we ask ourselves, have we visited all children of a parent of D? In this case, we have visited all children of A, so we can visit it. However, we can’t visit B yet, as we have not visited E (one of its children yet). Then we continue with the parents from F, and visit E. Have all the children of one of E’s parents been visited? Yes, of B, so we can visit it. Finally, we visit C.

Example #4

What will this snippet print?

class A:
    def __init__(self):
        print("I'm A's method")

class B(A):
    def __init__(self):
        print("I'm B's method")
        super().__init__()

class C(A):
    def __init__(self):
        print("I'm C's method")
        super().__init__()

class D(B, C):
    def __init__(self):
        print("I'm D's method")
        super().__init__()

if __name__ == "__main__":
    D()

Solution:

I'm D's method
I'm B's method
I'm C's method
I'm A's method

Explanation: super() calls also follow the MRO!! This means that, we are calling C’s __init__ method from B even though the classes are only connected through children and ancestors!!!!

That is, the super() call in D finds B’s constructor. The super() call in B, finds the constructor of the next class in the MRO, that is, C. Finally, the super() call in C, finds the constructor of A.

Oh Python, so intuitive and easy to learn :eyeroll:

Example #6

A more explicit and preferable version if calling ancestors’ constructors:

class A:
    def __init__(self):
        print("I'm A's method")

class B(A):
    def __init__(self):
        print("I'm B's method")
        A.__init__(self)

class C(A):
    def __init__(self):
        print("I'm C's method")
        A.__init__(self)

class D(B, C):
    def __init__(self):
        print("I'm D's method")
        B.__init__(self)

if __name__ == "__main__":
    D()

Solution:

I'm D's method
I'm B's method
I'm A's method

Explanation: This alternative way of calling the ancestors’ constructors directly uses the class reference available to call the method. In my opinion, this is a nicer, cleaner, more explicit version to deal with multiple inheritance constructor calls.

Takeaways

  • Multiple inheritence in Python can be a nightmare.
  • Python’s MRO does NOT follow breadth-first or depth-first ordering.
  • Use the class method mro to quickly understand complex inheritance trees.
  • super() calls follow the static class order defined by Python’s MRO!
  • Consider using Class.__init__ for more explicit parent constructor calls.
Written on January 19, 2023