How To Use Super() In Python

How To Use Super() In Python

ยท

7 min read

Introduction

Understanding how the super function works in python is pivotal to your understanding of multiple inheritance. This article walks you through:

  • super() and what it does.
  • how super behaves in multiple inheritance.
  • the Method Resolution Order (MRO) and how it dictates where super goes.
  • how to use super with arguments.

The article assumes you know how to create classes in Python.

super()

super is a function in Python that is used to call a method on another class. When a class inherits from another class, that class is said to be its parent class, base class, or superclass.

In simple inheritance, super is pretty straightforward because it goes directly to the parent class and calls the method if it's available:

class Base:
    def foo(self):
        return "Hello, from foo()"

class Child(Base):
    pass

>>>ch = Child()
>>>ch.foo()
"Hello, from foo()"

super gets even more interesting when used in multiple inheritance.

Multiple Inheritance

Multiple inheritance occurs when a class is derived or inherits from:

  • more than one class.
  • a class that inherits from two or more classes.

Multiple inheritance is useful in so many ways but can also be a source of pain and bugs if not properly implemented.

class Base:
    def foo(self):
        return 'foo() from Base'

class Aye(Base):
    pass
class Bee(Base):
    pass
class Cee(Aye, Bee):
    pass

In the code above:

  • Aye and Bee are subclasses of Base.
  • Cee inherits from Aye and Bee.
  • Cee has access to Base through Aye and Bee.
  • You can call the foo() method directly from an instance of Cee:
    >>>c = Cee()
    >>>c.foo()
    'foo() from Base'
    
    When you look at Cee's base classes, you only see classes Aye and Bee:
    >>>print(Cee.__bases__)
    (<class '__main__.Aye'>, <class '__main__.Bee'>)
    
    Base is not listed as one of the base classes of Cee, yet Base's foo() method can be called directly from Cee. Weird, right? The __bases__ attribute only returns a list of the classes that were declared as base classes during class definition. When you did:
    class Cee(Aye, Bee):
    
    __bases__ takes Aye and Bee as Cee's base classes. Cee gets access to Base's foo() method through an attribute: __mro__.

Method Resolution Order (MRO)

The MRO, as the name suggests, resolves the order in which a method is looked up in a class hierarchy. The way this happens is, Python computes an unduplicated, ordered list of classes for your multiple inheritance hierarchy. This list determines where super goes to call a method. A class's MRO list can be accessed through the __mro__ attribute. The algorithm Python uses to compute this list is called C3 Linearization. You can read about the algorithm here.

When you define a multiple inheritance, the MRO takes the class hierarchy and flattens it into a list. The MRO is computed such that subclasses come before their superclasses.

The Diamond Class Problem

class Base:
    def foo(self):
        return 'foo() from Base'

class Aye(Base):
    def foo(self):
        print('foo() from Aye')
        return super().foo()

class Bee(Base):
    def foo(self):
        print('foo() from Bee')
        return super().foo()

class Cee(Aye, Bee):
    def foo(self):
        print('foo() from Cee')
        return super().foo()

The code snippet above is an example of the diamond class hierarchy problem. All the classes have defined a foo() method, which prints a statement and then returns by calling super().foo(). Call foo() from Cee and see what the chain of super calls will be:

>>>c = Cee()
>>>c.foo()

Intuitively, invoking Cee.foo() should:

  • print a statement: foo() from Cee
  • call Aye's foo() method: foo() from Aye
  • call Base's foo() method: foo() from Bee

However, you get the following output:

foo() from Cee
foo() from Aye
foo() from Bee
foo() from Base

Why did the calls include Bee.foo()? Welcome to the weird world of the super function. Remember, subclasses are listed before their parents, on the MRO list. Since Cee inherits from Bee, which is a subclass of Base, Bee is checked first before Base. Take a look at class Cee's __mro__ attribute:

>>>print(Cee.__mro__)
(<class '__main__.Cee'>, <class '__main__.Aye'>, <class '__main__.Bee'>, 
<class '__main__.Base'>, <class 'object'>)

Cee's MRO is [Cee, Aye, Bee, Base]. When you call super from Cee, Python searches the method on the list, starting from the second class on the MRO list. super looks for the foo() method in:

  1. Aye ,
  2. then Bee,
  3. then Base,
  4. and finally, object.

super stops at the first class on the list that has the method it's called on. You can see that Aye and Bee come before Base on the list because they're subclasses of Base. So, in inheritance, try to think of super as a call to a method on the next class in the MRO list, instead of the class's parent or superclass.

One thing to pay attention to is the order in which the inheritance is defined. If you swap Aye and Bee, the MRO changes as well. So, the output instead becomes:

# class Cee(Bee, Aye):

foo() from Cee
foo() from Bee
foo() from Aye
foo() from Base

Passing Arguments to super

super can receive arguments. The first argument is a class, and the second, a class instance:

class Base:
    def foo(self):
        return 'foo() from Base'

class Aye(Base):
    def foo(self):
        print('foo() from Aye')
        return super().foo()

class Bee(Base):
    def foo(self):
        print('foo() from Bee')
        return super().foo()

class Cee(Aye, Bee):
    def foo(self):
        print('foo() from Cee')
        return super(Cee, self).foo()

The foo() method in class Cee calls super with arguments: super(Cee, self). This yields the same output as calling super without arguments. Passing arguments to super in the example above just says:

  1. Use Cee's MRO list.
  2. Look for foo() in one of the classes.
  3. If found, invoke it.

Passing arguments to superwas a requirement in Python 2, but not in Python 3.

Now, use Aye as argument instead of Cee:

class Base:
    def foo(self):
        return 'foo() from Base'

class Aye(Base):
    def foo(self):
        print('foo() from Aye')
        return super().foo()

class Bee(Base):
    def foo(self):
        print('foo() from Bee')
        return super().foo()

class Cee(Aye, Bee):
    def foo(self):
        print('foo() from Cee')
        return super(Aye, self).foo() #change

The change yields the following:

>>>c = Cee()
>>>c.foo()

foo() from Cee
foo() from Bee
'foo() from Base'

You may have noticed that "foo from Aye" was not printed. This is because super started searching for foo() from the next class after Aye on Cee's MRO list. Remember, Bee comes after Aye on the list, hence Aye was skipped.

The MRO list is consistent throughout the calls because super uses the list of the class initiating the call. Take a look at every class's MRO list for comparison:

>>>Cee.__mro__
(<class '__main__.Cee'>, <class '__main__.Aye'>, <class '__main__.Bee'>, <class '__main__.Base'>, <class 'object'>)

>>>Aye.__mro__
(<class '__main__.Aye'>, <class '__main__.Base'>, <class 'object'>)

>>>Bee.__mro__
(<class '__main__.Bee'>, <class '__main__.Base'>, <class 'object'>)

>>>Base.__mro__
<class '__main__.Base'>, <class 'object'>)

You can call super directly outside of a method with a class and an instance of a class as arguments:

>>>c=Cee()
>>>super(Cee, c).foo()

foo() from Aye
foo() from Bee
'foo() from Base'

There are instances when calling super with arguments leads to an infinite recursion. Let's see how that may happen:

class Base:
    def foo(self):
        return 'foo() from Base'

class Aye(Base):
    def foo(self):
        print('foo() from Aye')
        return super(Cee, self).foo() #change

class Bee(Base):
    def foo(self):
        print('foo() from Bee')
        return super().foo()

class Cee(Aye, Bee):
    def foo(self):
        print('foo() from Cee')
        return super().foo()

In the code snippet above, Aye.foo() calls super with Cee as its first argument. Since the call was initiated by Cee, super uses Cee's MRO list: [Cee, Aye, Bee, Base]. Calling Cee.foo() leads to an infinite recursion because of the following:

  1. The call starts from Cee.foo(), which prints "foo() from Cee"
  2. Then calls super().foo(), which goes to Aye.foo().
  3. Aye.foo() prints foo() from Aye, then calls super().foo() with Cee and self as its arguments.
  4. super looks at the MRO list and jumps to the next class after Cee on the list to call its foo() method, but that class happens to be Aye itself.
  5. So, it goes back to Aye.foo() and steps 3 and 4 are repeated. This becomes an infinite loop, hence the interpreter terminates the call with a RecursionError exception.

As you can see, passing arguments to super can be very dangerous and should be avoided. Unless you have a very good reason for doing this, you should avoid this evil practice ๐Ÿ˜ญ.

Summary

Python provides a rich toolset for Object-Oriented Programming. One of the most important parts in that set is the super function. Hopefully, this article has helped you better understand how to use the super function.

Thank you for reading! If you have any questions, I'd be happy to answer them.