Home | Articles | CV (pdf | short)
<2022-12-02> by Lorenzo

Python's MRO surprises

Consider a script like this:

class A:
    def __init__(self):
        print('a')

class B:
    def __init__(self):
        print('b')

class C(A, B):
    def __init__(self):
        super().__init__()
        print('c')

C()

What will it output? If you are like me, you would answer, without much thought, that it prints:

a
c

To work it out you need to know about Python's MRO (Method Resolution Order): this article provides a clear explanation of how it works. In this case, we have:

>>> C.mro()
[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]

Now try this other snippet. The only change is a call to super().__init__() in class A.

class A:
    def __init__(self):
        super().__init__()
        print('a')

class B:
    def __init__(self):
        print('b')

class C(A, B):
    def __init__(self):
        super().__init__()
        print('c')

C()

What will it print? Again, if you are like me you will expect to print the same. But it doesn't. It prints

b
a
c

I find it surprising in many ways. First of all, I am surprised that MRO is affected by the body of __init__(): I would have thought it is statically determined by the class hierarchy. Secondly, I would have thought to see 'a b c' printed, not 'b a c'. Note that if you swap the two lines of A.__init__, so that super() is the last statement, it indeed prints 'a b c'.

Finally, consider this script:

class A:
    def __init__(self):
        print('a')

class B:
    def __init__(self):
        super().__init__()
        print('b')

class C(A, B):
    def __init__(self):
        super().__init__()
        print('c')

C()

What will it print? I guessed right this time:

a
c

I found a nice explanation in this blog post, so please read it for details. The way I think about it is that MRO uses a kind-of BFS to linearise the inheritance graph of C: first it looks at A, but as soon as A.__init__ is called, it finds a call to super() which causes to temporarily stop the search there and visit the sibling of A, which is B. At this point, 'b' is printed and execution of A.__init__ is "resumed", so 'a' is printed and finally 'c'.

It's a complicated algorithm to fit in one's mind, I reckon. And the result depends on the implementation of __init__, which may not be readily available to read: so how will one know what will happen? The recommendation I got from various sources is: "Avoid multiple inheritance, especially in Python". It seems like good advice.