Skip to main content

Intro to Multiple Inheritance & super()

A Pythonista's introductory guide to multiple inheritance, the super() function, & how to navigate the diamond problem.
Feb 2019  · 8 min read

Quick overview of inheritance

As you grow your Python projects and packages, you'll inevitably want to utilize classes and apply the DRY (don't-repeat-yourself) principle while doing so. Class inheritance is a fantastic way to create a class based on another class in order to stay DRY. This post will cover more advanced concepts of inheritance, and basic inheritance won't be covered in depth. We'll go over a quick intro, but there are much better, detailed introductions out there. Here are some resources to get started: Object-Oriented Programming in Python course & Python Object-Oriented Programming (OOP): Tutorial.

So what is class inheritance? Similarly to genetics, a child class can 'inherit' attributes and methods from a parent. Let's jump right into some code for an example. In the below code block we'll demonstrate inheritance with a Child class inheriting from a Parent class.

Input

class Parent:
    def __init__(self):
        self.parent_attribute = 'I am a parent'

    def parent_method(self):
        print('Back in my day...')


# Create a child class that inherits from Parent
class Child(Parent):
    def __init__(self):
        Parent.__init__(self)
        self.child_attribute = 'I am a child'


# Create instance of child
child = Child()

# Show attributes and methods of child class
print(child.child_attribute)
print(child.parent_attribute)
child.parent_method()

Output

I am a child
I am a parent
Back in my day...

We see that the Child class 'inherited' attributes and methods from the Parent class. Without any work on our part, the Parent.parent_method is a part of the Child class. To get the benefits of the Parent.__init__() method we needed to explicitly call the method and pass self. This is because when we added an __init__ method to Child, we overwrote the inherited __init__.

With that brief, very non-comprehensive overview out of the way lets jump into the meat of the post.

Intro to super

In the simplest case, the super function can be used to replace the explicit call to Parent.__init__(self). Our intro example from the first section can be rewritten with super as seen below. Note, that the below code block is written in Python 3, earlier versions use a slightly different syntax. Additionally, the output has been omitted since it's identical to the first code block.

class Parent:
    def __init__(self):
        self.parent_attribute = 'I am a parent'

    def parent_method(self):
        print('Back in my day...')


# Create a child class that inherits from Parent
class Child(Parent):
    def __init__(self):
        super().__init__()
        self.child_attribute = 'I am a parent'


# Create instance of child
child = Child()

# Show attributes and methods of child class
print(child.child_attribute)
print(child.parent_attribute)
child.parent_method()

To be honest, super in this case gives us little, if any, advantage. Depending on the name of our parent class we might save some keystrokes, and we don't have to pass self to the call to __init__. Below are some pros and cons of the use of super in single inheritance cases.

Cons

It can be argued that using super here makes the code less explicit. Making code less explicit violates The Zen of Python, which states, "Explicit is better than implicit."

Pros

There is a maintainability argument that can be made for super even in single inheritance. If for whatever reason your child class changes its inheritance pattern (i.e., parent class changes or there's a shift to multiple inheritance) then there's no need find and replace all the lingering references to ParentClass.method_name(); the use of super will allow all the changes to flow through with the change in the class statement.

super and multiple inheritance

Before we get into multiple inheritance and super... Warning, this can get pretty weird and complicated.

First off, what is multiple inheritance? So far the example code has covered a single child class inheriting from a single parent class. In multiple inheritance, there's more than one parent class. A child class can inherit from 2, 3, 10, etc. parent classes.

Here is where the benefits of super become more clear. In addition to saving keystrokes of referencing the different parent class names, there are nuanced benefits to using super with multiple inheritance patterns. In short, if you're going to use multiple inheritance, use super.

Multiple inheritance without super

Let's look at an example of multiple inheritance that avoids modifying any parent methods and in turn avoids super.

Input

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


class C:
    def c(self):
        print('c')


class D(B, C):
    def d(self):
        print('d')


d = D()
d.b()
d.c()
d.d()

Output

b
c
d

Multiple-resolution order

This output isn't too surprising given the concept of multiple inheritance. D inherited the methods x and z from its parent classes, and everything is good in the world... for now.

So what if both B and C both had a method with the same name? This is where a concept called 'multiple-resolution order' comes into play or MRO for short. The MRO of a child class is what decides where Python will look for a given method, and which method will be called when there's a conflict.

Let's look at an example.

Input

class B:
    def x(self):
        print('x: B')


class C:
    def x(self):
        print('x: C')


class D(B, C):
    pass


d = D()
d.x()
print(D.mro())

Output

x: B
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class 'object'>]

When we call the inherited x method, we only see the output inherited from B. We can see the MRO of our D class by calling the mro class method. From the D.mro() output we learn the following: our program will try to call D methods by default, then resort to B, then C, and finally object. If it's not found in any of those places, then we'll get the error that D doesn't have the method we asked for.

It's worth noting that by default, every class inherits from object, and it's on the tail-end of every MRO.

Multiple inheritance, super, and the diamond problem

Below is an example of using super to handle MRO of init in a way that's beneficial. In the example, we create a series of text processing classes and combine their functionality in another class with multiple inheritance. We'll create 4 classes, and the structure for inheritance will follow the structure in the below diagram.

Note: This structure is for illustrative purposes, and, barring restraints, there would be better ways to implement this logic.

diamond problem

This is actually an example of the 'diamond problem' of multiple inheritance. Its name is of course based on the shape of its design, and the fact that it's a fairly confusing problem.

Below the design is written out with the use of super.

Input

class Tokenizer:
    """Tokenize text"""
    def __init__(self, text):
        print('Start Tokenizer.__init__()')
        self.tokens = text.split()
        print('End Tokenizer.__init__()')


class WordCounter(Tokenizer):
    """Count words in text"""
    def __init__(self, text):
        print('Start WordCounter.__init__()')
        super().__init__(text)
        self.word_count = len(self.tokens)
        print('End WordCounter.__init__()')


class Vocabulary(Tokenizer):
    """Find unique words in text"""
    def __init__(self, text):
        print('Start init Vocabulary.__init__()')
        super().__init__(text)
        self.vocab = set(self.tokens)
        print('End init Vocabulary.__init__()')


class TextDescriber(WordCounter, Vocabulary):
    """Describe text with multiple metrics"""
    def __init__(self, text):
        print('Start init TextDescriber.__init__()')
        super().__init__(text)
        print('End init TextDescriber.__init__()')


td = TextDescriber('row row row your boat')
print('--------')
print(td.tokens)
print(td.vocab)
print(td.word_count)

Output

Start init TextDescriber.__init__()
Start WordCounter.__init__()
Start init Vocabulary.__init__()
Start Tokenizer.__init__()
End Tokenizer.__init__()
End init Vocabulary.__init__()
End WordCounter.__init__()
End init TextDescriber.__init__()
--------
['row', 'row', 'row', 'your', 'boat']
{'boat', 'your', 'row'}
5

First off, we see the TextDescriber class has inherited all the attributes of the class family tree. Thanks to multiple inheritance we can 'combine' the functionality of more than one class.

Let's now discuss the printouts that came from the class's init methods:

Each __init__ method was called once and only once.

The TextDescriber class inherited from 2 classes that inherit from Tokenizer. Why was Tokenizer.__init__ not called twice?

If we replaced all of our calls to super with the old fashioned way, we would end up with 2 calls to Tokenizer.__init__. The calls to super 'think' through our pattern a little bit more and skips the extra trip to A.

Each __init__ method was started before any of the others were finished.

The order of the starts and finishes of each __init__ is worth noting in case you're attempting to set an attribute that has a naming conflict with another parent class. The attribute will be overwritten, and it can become very confusing.

In our case, we avoided naming conflicts with inherited attributes, so everything is working as expected.

To reiterate, the diamond problem can get complicated fast and lead to unexpected results. With most cases in programming, it's best to avoid complicated designs.

What we learned

  • We learned about the super function and how it can be used to replace ParentName.method in single inheritance. This can be a more maintainable practice.
  • We learned about multiple inheritance and how we can pass on the functionality of multiple parent classes to a single child class.
  • We learned about multiple-resolution order and how it decides what happens in multiple inheritance when there's a naming conflict between parent methods.
  • We learned about the diamond problem and saw an example of how the use of super navigates the diamond.

Introduction to Python

Beginner
4 hours
4,584,161
Master the basics of data analysis with Python in just four hours. This online course will introduce the Python interface and explore popular packages.
See DetailsRight Arrow
Start Course

Intermediate Python

Beginner
4 hours
879,952
Level up your data science skills by creating visualizations using Matplotlib and manipulating DataFrames with pandas.

Python Data Science Toolbox (Part 1)

Beginner
3 hours
343,445
Learn the art of writing your own functions in Python, as well as key concepts like scoping and error handling.
See all coursesRight Arrow
Related

The 23 Top Python Interview Questions & Answers

Essential Python interview questions with examples for job seekers, final-year students, and data professionals.
Abid Ali Awan's photo

Abid Ali Awan

22 min

Working with Dates and Times in Python Cheat Sheet

Working with dates and times is essential when manipulating data in Python. Learn the basics of working with datetime data in this cheat sheet.
DataCamp Team's photo

DataCamp Team

Plotly Express Cheat Sheet

Plotly is one of the most widely used data visualization packages in Python. Learn more about it in this cheat sheet.
DataCamp Team's photo

DataCamp Team

0 min

Getting started with Python cheat sheet

Python is the most popular programming language in data science. Use this cheat sheet to jumpstart your Python learning journey.
DataCamp Team's photo

DataCamp Team

8 min

Python pandas tutorial: The ultimate guide for beginners

Are you ready to begin your pandas journey? Here’s a step-by-step guide on how to get started. [Updated November 2022]
Vidhi Chugh's photo

Vidhi Chugh

15 min

Python Iterators and Generators Tutorial

Explore the difference between Python Iterators and Generators and learn which are the best to use in various situations.
Kurtis Pykes 's photo

Kurtis Pykes

10 min

See MoreSee More