many people will tell you super() is a nifty tool in python; it allows you to call a method in super class without specifying super class name:

##  example 0

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

class B(A):
    def __init__(self):
        super().__init__()  # instead of `A.__init__(self)`
        print('B')

B()

output:

A
B

this allows you to change super class name while using the same code at call site;

however, if you think super() means to return the super class, you are horribly wrong; the problem manifests itself when you start programming with multiple inheritance;

a simple counter example is to extend the above example like this:

##  example 1

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

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

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

class D(B, C): pass

D()

output:

C
B

now you can see super() in B doesnt direct to A but C;

demystify super

in python, each class has a method resolution order; it defines the order of method search; for example, in example 1, the mro of class D is DBCA; the class next to B is C; that is why super() in B refers to C (not A);

in fact, the signature of super() is super(class, mro_class), whose exact semantic is:

  • return a proxy object that delegates method calls to the next class of class in the mro of mro_class;

for example, if D extends C extends B extends A, then D has mro DCBA, so super(C, D) returns B; note that, for sake of brevity, in this article we do not distinguish the returned proxy object and its proxied object; you should know super(C, D) is a proxy of B, not B itself;

call signatures

the classic form of super() is super(class, mro_class); we recommend you to use this form whenever possible; in addition, super() also has several other call signatures:

  1. super(class, mro_obj):

    super() allows to use a non-type object mro_obj as the second parameter; in this form, the mro is inferred from the class of mro_obj; plus, proxied method calls are bound to mro_obj;

  2. super(class):

    super() allows to omit the second parameter; in this form, the returned proxy object is an unbound super; this is very tricky and it is recommended not to use this form at all;

  3. super():

    super() allows to omit both parameters when used inside class definition; in this form, the compiler fills in the missing parameters:

    class C(B):
        def method(self, arg):
            super().method(arg)    # This does the same thing as:
                                   # super(C, self).method(arg)
    

solution

now back to our problematic example 1; the problem happens because:

  • when we call super().__init__() in B.__init__, we mean to call A.__init__;

  • however, in our multiple inheritance heterarchy, super(), which is equivalent to super(B, self), works like (a bound version of) super(B, D) (because self is an instance of D), and resolves to C (because D has mro DBCA);

to fix this, we need to pass super() the correct mro (B, not D):

##  example 2

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

class B(A):
    def __init__(self):
        super(B, B).__init__(self)  # super(B, B) not super(B, D)
        print('B')

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

class D(B, C): pass

D()

output:

A
B

conclusion

  • if you use super() to indirectly name super class, you should always use its two-argument form super(class, mro_class) with both classes set to the current class (like super(X, X)); this limits method resolution to the class itself and its super classes (a property which i call forward secrecy);

    within methods, you can also use super(__class__, __class__) to avoid explicit class names:

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

    __class__ is an implicit closure reference created by the compiler if any methods in a class body refer to either __class__ or super.

  • if you use super(), in its zero-argument form, to support cooperative multiple inheritance, i suggest you give up this idea unless you really really need it and your classes are only to be used by yourself;

    the reason is, as you have seen in example 0 and 1, you do not know which class your super() resolves to when you are writing class B; users who extend class B need to check super() calls are wired correctly across the whole class heterarchy, which can be huge burden and inflexibility;

  • unfortunately, python uses super() by default, which means if you omit a method override, a default implemention based on super() is assumed:

    class B(A): pass
    

    works like:

    class B(A):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
    

    this effectively passes the method call to the next candidate in the object mro, producing a no-op effect; this is nifty with the assumed cooperative multiple inheritance paradiam, but is at a discrepancy from the idea that each class only cares about itself and its own superclasses;

combining all these points, i came to the conclusion that super()-based cooperative multiple inheritance in python is an anti-pattern; the idea of cooperative multiple inheritance (broad sight) and the idea of forward secrecy (narrowed sight) cannot be easily reconciled; furthermore, super() in python is a misnomer; if you want to use it correctly, remember this all the time: super() means next, not parent;