oop – What does super do in Python? – difference between super().__init__() and explicit superclass __init__()

oop – What does super do in Python? – difference between super().__init__() and explicit superclass __init__()

Whats the difference?

SomeBaseClass.__init__(self) 

means to call SomeBaseClasss __init__. while

super().__init__()

means to call a bound __init__ from the parent class that follows SomeBaseClasss child class (the one that defines this method) in the instances Method Resolution Order (MRO).

If the instance is a subclass of this child class, there may be a different parent that comes next in the MRO.

Explained simply

When you write a class, you want other classes to be able to use it. super() makes it easier for other classes to use the class youre writing.

As Bob Martin says, a good architecture allows you to postpone decision making as long as possible.

super() can enable that sort of architecture.

When another class subclasses the class you wrote, it could also be inheriting from other classes. And those classes could have an __init__ that comes after this __init__ based on the ordering of the classes for method resolution.

Without super you would likely hard-code the parent of the class youre writing (like the example does). This would mean that you would not call the next __init__ in the MRO, and you would thus not get to reuse the code in it.

If youre writing your own code for personal use, you may not care about this distinction. But if you want others to use your code, using super is one thing that allows greater flexibility for users of the code.

Python 2 versus 3

This works in Python 2 and 3:

super(Child, self).__init__()

This only works in Python 3:

super().__init__()

It works with no arguments by moving up in the stack frame and getting the first argument to the method (usually self for an instance method or cls for a class method – but could be other names) and finding the class (e.g. Child) in the free variables (it is looked up with the name __class__ as a free closure variable in the method).

I used to prefer to demonstrate the cross-compatible way of using super, but now that Python 2 is largely deprecated, I will demonstrate the Python 3 way of doing things, that is, calling super with no arguments.

Indirection with Forward Compatibility

What does it give you? For single inheritance, the examples from the question are practically identical from a static analysis point of view. However, using super gives you a layer of indirection with forward compatibility.

Forward compatibility is very important to seasoned developers. You want your code to keep working with minimal changes as you change it. When you look at your revision history, you want to see precisely what changed when.

You may start off with single inheritance, but if you decide to add another base class, you only have to change the line with the bases – if the bases change in a class you inherit from (say a mixin is added) youd change nothing in this class.

In Python 2, getting the arguments to super and the correct method arguments right can be a little confusing, so I suggest using the Python 3 only method of calling it.

If you know youre using super correctly with single inheritance, that makes debugging less difficult going forward.

Dependency Injection

Other people can use your code and inject parents into the method resolution:

class SomeBaseClass(object):
    def __init__(self):
        print(SomeBaseClass.__init__(self) called)
    
class UnsuperChild(SomeBaseClass):
    def __init__(self):
        print(UnsuperChild.__init__(self) called)
        SomeBaseClass.__init__(self)
    
class SuperChild(SomeBaseClass):
    def __init__(self):
        print(SuperChild.__init__(self) called)
        super().__init__()

Say you add another class to your object, and want to inject a class between Foo and Bar (for testing or some other reason):

class InjectMe(SomeBaseClass):
    def __init__(self):
        print(InjectMe.__init__(self) called)
        super().__init__()

class UnsuperInjector(UnsuperChild, InjectMe): pass

class SuperInjector(SuperChild, InjectMe): pass

Using the un-super child fails to inject the dependency because the child youre using has hard-coded the method to be called after its own:

>>> o = UnsuperInjector()
UnsuperChild.__init__(self) called
SomeBaseClass.__init__(self) called

However, the class with the child that uses super can correctly inject the dependency:

>>> o2 = SuperInjector()
SuperChild.__init__(self) called
InjectMe.__init__(self) called
SomeBaseClass.__init__(self) called

Addressing a comment

Why in the world would this be useful?

Python linearizes a complicated inheritance tree via the C3 linearization algorithm to create a Method Resolution Order (MRO).

We want methods to be looked up in that order.

For a method defined in a parent to find the next one in that order without super, it would have to

  1. get the mro from the instances type
  2. look for the type that defines the method
  3. find the next type with the method
  4. bind that method and call it with the expected arguments

The UnsuperChild should not have access to InjectMe. Why isnt the conclusion Always avoid using super? What am I missing here?

The UnsuperChild does not have access to InjectMe. It is the UnsuperInjector that has access to InjectMe – and yet cannot call that classs method from the method it inherits from UnsuperChild.

Both Child classes intend to call a method by the same name that comes next in the MRO, which might be another class it was not aware of when it was created.

The one without super hard-codes its parents method – thus is has restricted the behavior of its method, and subclasses cannot inject functionality in the call chain.

The one with super has greater flexibility. The call chain for the methods can be intercepted and functionality injected.

You may not need that functionality, but subclassers of your code may.

Conclusion

Always use super to reference the parent class instead of hard-coding it.

What you intend is to reference the parent class that is next-in-line, not specifically the one you see the child inheriting from.

Not using super can put unnecessary constraints on users of your code.

The benefits of super() in single-inheritance are minimal — mostly, you dont have to hard-code the name of the base class into every method that uses its parent methods.

However, its almost impossible to use multiple-inheritance without super(). This includes common idioms like mixins, interfaces, abstract classes, etc. This extends to code that later extends yours. If somebody later wanted to write a class that extended Child and a mixin, their code would not work properly.

oop – What does super do in Python? – difference between super().__init__() and explicit superclass __init__()

I had played a bit with super(), and had recognized that we can change calling order.

For example, we have next hierarchy structure:

    A
   / 
  B   C
    /
    D

In this case MRO of D will be (only for Python 3):

In [26]: D.__mro__
Out[26]: (__main__.D, __main__.B, __main__.C, __main__.A, object)

Lets create a class where super() calls after method execution.

In [23]: class A(object): #  or with Python 3 can define class A:
...:     def __init__(self):
...:         print(Im from A)
...:  
...: class B(A):
...:      def __init__(self):
...:          print(Im from B)
...:          super().__init__()
...:   
...: class C(A):
...:      def __init__(self):
...:          print(Im from C)
...:          super().__init__()
...:  
...: class D(B, C):
...:      def __init__(self):
...:          print(Im from D)
...:          super().__init__()
...: d = D()
...:
Im from D
Im from B
Im from C
Im from A

    A
   / ⇖
  B ⇒ C
   ⇖ /
    D

So we can see that resolution order is same as in MRO. But when we call super() in the beginning of the method:

In [21]: class A(object):  # or class A:
...:     def __init__(self):
...:         print(Im from A)
...:  
...: class B(A):
...:      def __init__(self):
...:          super().__init__()  # or super(B, self).__init_()
...:          print(Im from B)
...:   
...: class C(A):
...:      def __init__(self):
...:          super().__init__()
...:          print(Im from C)
...:  
...: class D(B, C):
...:      def __init__(self):
...:          super().__init__()
...:          print(Im from D)
...: d = D()
...: 
Im from A
Im from C
Im from B
Im from D

We have a different order it is reversed a order of the MRO tuple.

    A
   / ⇘
  B ⇐ C
   ⇘ /
    D 

For additional reading I would recommend next answers:

  1. C3 linearization example with super (a large hierarchy)
  2. Important behavior changes between old and new style classes
  3. The Inside Story on New-Style Classes

Leave a Reply

Your email address will not be published.