MRO and accessing methods from superclasses

super is a built-in class that can be used to access an attribute belonging to an object's superclass.

The Python official documentation lists   super  as a built-in function, but, it's a built-in class, even if it is used like a function:
>>> super
<class 'super'>
>>> isinstance(super, type)

Its usage is a bit confusing if you are used to accessing a class attribute or method by calling the parent class directly and passing self as the first argument. This is a really old pattern, but still can be found in some code bases (especially in legacy projects). See the following code:

class Mama:  # this is the old way 
    def says(self): 
        print('do your homework') 
 
          
class Sister(Mama): 
    def says(self): 
        Mama.says(self) 
        print('and clean your bedroom')

Look particularly at the Mama.says(self) line. You can see here an explicit use of parent class. This means that the says() method belonging to Mama will be called. But, the instance on which it will be called is provided as the self argument, which is an instance of Sister in this case.

Instead, the super usage would be as follows:

class Sister(Mama):
def says(self):
super(Sister, self).says() print('and clean your bedroom')

Alternatively, you can also use the shorter form of the super() call:

class Sister(Mama):
def says(self):
super().says() print('and clean your bedroom')

The shorter form of super (without passing any arguments) is allowed inside the methods, but the usage of super is not limited to the body of methods. It can be used in any code area where the explicit call to the method of superclass implementation is required. Still, if super is not used inside the body of the method, then, all of its arguments are mandatory:

    >>> anita = Sister()
    >>> super(anita.__class__, anita).says()
    do your homework
  

The final and most important thing that should be noted about super is that its second argument is optional. When only the first argument is provided, then super returns an unbounded type. This is especially useful when working with classmethod:

class Pizza: 
    def __init__(self, toppings): 
        self.toppings = toppings 
 
    def __repr__(self): 
        return "Pizza with " + " and ".join(self.toppings) 
 
    @classmethod 
    def recommend(cls): 
        """Recommend some pizza with arbitrary toppings,""" 
        return cls(['spam', 'ham', 'eggs']) 
 
 
class VikingPizza(Pizza): 
    @classmethod 
    def recommend(cls): 
        """Use same recommendation as super but add extra spam""" 
        recommended = super(VikingPizza).recommend() 
        recommended.toppings += ['spam'] * 5 
        return recommended 

Note that the zero-argument super() form is also allowed for methods decorated with the classmethod decorator. super(), if called without arguments in such methods, is treated as having only the first argument defined.

The use cases presented earlier are very simple to follow and understand, but when you face a multiple inheritance schema, it becomes hard to use super. Before explaining these problems, you need to first understand when super should be avoided and how the Method Resolution Order (MRO) works in Python.

Let's discuss old-style classes and super in Python 2.