sangeeta.io
Feb 16, 2021 • 30 min read

A Super-Post on Python Inheritance

Inheritance is an object oriented feature which allows us to reuse logic by defining parent classes that pass on certain behavior to subclasses.

In this article, we’ll cover some examples of inheritance with Python, the use of the super() function, and some practical examples of inheritance through the use of mixins. The focus will be more on understanding the complexities of multiple inheritance.

Feel free to skip ahead to any section, but I recommend reading in order:

Inheritance 101 👨‍👩‍👧‍👦

Subclasses inherit all attributes and methods from a parent class if they don’t override them directly.

class Person:
    def __init__(self, name):
        self.name = name

    def intro(self):
        return f'Hi I\'m {self.name}!'


class Employee(Person):
    pass

In this example any Employee instance will have a name attribute and an intro method attached to it. Why? Because when we create an Employee instance, it doesn’t define its own __init__ or intro methods, so it uses its parents’.

>>> e = Employee('Sangeeta')
>>> e
<__main__.Employee object at 0x100774520>
>>> e.name
'Sangeeta'
>>> e.intro
<bound method Person.intro of <__main__.Employee object at 0x100774520>>
>>> e.intro()
"Hi I'm Sangeeta!"

If we did define an empty __init__ method on Employee, then the Person’s __init__ method will never get called, and Employee will never inherit the name attribute from Person.

class Person:
    def __init__(self, name):
        self.name = name

    def intro(self):
        return f'Hi I\'m {self.name}!'


class Employee(Person):
    def __init__(self):
        pass

When we create an Employee instance, we still have access to Person’s intro method, so technically we can set a name attribute afterwards and call intro successfully.

>>> e = Employee()
>>> e
<__main__.Employee object at 0x109bd7640>
>>> e.intro
<bound method Person.intro of <__main__.Employee object at 0x109bd7640>>
>>> e.intro()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/sangeeta/Code/pystuff/super.py", line 111, in intro
    return f'Hi I\'m {self.name}!'
AttributeError: 'Employee' object has no attribute 'name'
>>>
>>> e.name = 'Sangeeta'   # Finally setting a name attribute
>>> e.intro()
"Hi I'm Sangeeta!"

The parent class attributes and methods are inherited by default if they are not explicitly defined in the subclass

Let’s drive this point home with another example, whereby both Person and Employee’s __init__ set the same name attribute, but this time the Person’s name attribute is uppercased.

class Person:
    def __init__(self, name):
        self.name = name.upper()

    def intro(self):
        return f'Hi I\'m {self.name}!'


class Employee(Person):
    def __init__(self, name):
        self.name = name

Our Employee class uses its own name attribute, not the Person’s name attribute.

>>> e = Employee('Sangeeta')
>>> e
<__main__.Employee object at 0x101a3b640>
>>> e.name
'Sangeeta'
>>> e.intro()
"Hi I'm Sangeeta!"
>>>
>>>
>>> p = Person('Sangeeta')
>>> p
<__main__.Person object at 0x101a3b700>
>>> p.name
'SANGEETA'
>>> p.intro()
"Hi I'm SANGEETA!"

Multiple Inheritance 🥞

When a class inherits from multiple classes, its behavior is directly determined by the order of parent classes in the class definition. Let’s take a look at an example first:

class Person:
    def __init__(self, name):
        self.name = name

    def intro(self):
        return f'Hi I\'m {self.name}!'


class Pythonista:
    def __init__(self, name):
        self.name = name

    def intro(self):
        return f'Hi I\'m {self.name} and I love Python!'


class Employee(Person, Pythonista):
    pass

In this example, a call to Employee’s intro method will call the Person’s intro method because it is the first class of the inheritance chain (from left to right) that has an intro method defined. The golden rule is

If a method is called, Python will look for that method in the class. If it doesn't find it, it will look in the next class listed in this fancy thing called the MRO until it finds it. If it doesn't find it in that chain, it will raise an AttributeError.

In this example, Pythonista’s methods will actually never be used since Person defines the same methods as Pythonista. In other words, any call to an Employee instance’s __init__ and intro will always refer to the Person class.

MRO OMG WTF 😳

A big part of the inheritance picture for me was understanding that inheritance is always in the context of the MRO. In fact, all the examples we’ve seen so far have been making use of MRO, but now lets take a closer look at what it is and how it works in more complex examples.

In our Employee example, the order of parent classes in the class definition defines the order in which method resolution happens. This is precisely MRO – Method Resolution Order – a list that Python constructs to figure our which parent class to use to lookup a method!

class Person:
    pass


class Pythonista:
    pass


class Employee(Person, Pythonista):
    pass
>>> Employee.mro()
[<class '__main__.Employee'>, <class '__main__.Person'>, <class '__main__.Pythonista'>, <class 'object'>]

💡 Bonus tip: Since we are using Python3 for these examples, the Person and Pythonista implicitly inherit from object, so in fact class Person: pass is the same as class Person(object): pass.

There are some intricacies in how the MRO is created. What if Person and Pythonista both inherited from other classes? Let’s look at a different example:

class Human:
    pass


class Person(Human):
    pass


class Programmer:
    pass


class Pythonista(Programmer):
    pass


class Employee(Person, Pythonista):
    pass
>>> Employee.mro()
[<class '__main__.Employee'>, <class '__main__.Person'>, <class '__main__.Human'>, <class '__main__.Pythonista'>, <class '__main__.Programmer'>, <class 'object'>]

When constructing the MRO, Python uses an algorithm called the C3 linearization algorithm to go through the inheritance chain of each parent class and condense the entire list in a way that conforms to certain constraints. The rule of thumb is that ordering should occur from the most specific class listed down to its parents, and child classes must precede their parents.

Below shows how Employee’s MRO is constructed from the example above:

Employee inheritance example 1

As we can see, object (the parent of all classes in Python) appears twice, so following the rule that children precede parents, the rightmost instance of object remains and the other duplicate is removed.

Let’s see another example:

class Human:
    pass


class Person(Human):
    pass


class Pythonista(Human):
    pass


class Employee(Person, Pythonista):
    pass

In this case, both Person and Pythonista inherit from the same parent class, so the MRO for Employee would be formed like this:

Our Employee inheritance example 2

>>> Employee.mro()
[<class '__main__.Employee'>, <class '__main__.Person'>, <class '__main__.Pythonista'>, <class '__main__.Human'>, <class 'object'>]

Be mindful that there is a wrong way to inherit, in which Python will throw a nice little TypeError exception at you. Let’s take a look at such a case:

class Human:
    pass


class Person(Human):
    pass


class Employee(Human, Person):
    pass

If we try to create an Employee instance, we get hit with this:

Traceback (most recent call last):
  File "/Users/sangeeta/Code/pystuff/super.py", line 133, in <module>
    class Employee(Human, Person):
TypeError: Cannot create a consistent method resolution
order (MRO) for bases Human, Person

With the MRO rule that children precede parents, you’d think the MRO would resolve itself but we run into some problems:

Our Employee inheritance example 3

The problem is that Human is a direct parent class of Employee so it cannot be thrown to the right just because Person also subclasses from it; it needs to stay there because its a direct parent. This breaks the other constraint that child classes should precede their parents. In this case, Python cannot satisfy all constraints of the MRO, therefore it throws an exception. Refer to this StackOverflow answer for a more detailed answer to this problem.

If you ever come across this, you should probably reconsider your inheritance choices and logic decisions. Fortunately, Python equips us with a powerful function called super to give us the ability to call parent methods within child class methods and create better logic flow when subclassing.

super duper super 💥

Inheritance is great because we inherit functionality from parent classes into our subclasses. As it is, we can either

But sometimes, in more real world scenarios, we want both! We want to tap into the parent class’s logic and insert that either before or after our custom logic. We can achieve just that with the super function.

Calling super() within a subclass method will lookup the next class in the MRO which has that method defined and call it.

The following example is bit of a no-op illustration of what calling super does. Both implementations of the Employee class behave the same.

class Person:
    def __init__(self, name):
        self.name = name

    def intro(self):
        return f'Hi I\'m {self.name}!'


###### Implicit ######
class Employee1(Person):
    pass


###### Explicit ######
class Employee2(Person):
    def __init__(self, name):
        super().__init__(name)

    def intro(self):
        return super().intro()

In Employee2, we are overriding the __init__ and intro methods but we are calling super inside each one of them to reference the corresponding parent method. Additionally, we didn’t define any custom logic in those methods, so the behavior is identical to Employee1.

>>> e1 = Employee1('Sangeeta')
>>> e2 = Employee2('Sangeeta')
>>>
>>> Employee1.mro()
[<class '__main__.Employee1'>, <class '__main__.Person'>, <class 'object'>]
>>>
>>> Employee2.mro()
[<class '__main__.Employee2'>, <class '__main__.Person'>, <class 'object'>]
>>>
>>> e1.intro()
"Hi I'm Sangeeta!"
>>>
>>> e2.intro()
"Hi I'm Sangeeta!"
>>>

💡 Bonus tip: In Python3, calling super().intro() is the same as calling super with parameters like so super(Employee, self).intro().

Let’s override some methods on the Employee class to have its own functionality while borrowing the needed bits from its parent.

class Person:
    def __init__(self, name):
        self.name = name

    def intro(self):
        return f'Hi I\'m {self.name}!'


class Employee(Person):
    def __init__(self, name, manager):
        super().__init__(name)
        self.manager = manager

    def intro(self):
        return (
            f'{super().intro()} I\'m on the tech team '
            f'and I report to {self.manager}.'
        )

In Employee’s __init__ method, we pass on the name attribute to Person’s __init__ and retain the manager attribute on Employee. In the Employee’s intro method, we capture the return value of Person’s intro call and combine it with some custom message.

>>> e = Employee('Sangeeta', 'Luke Skywalker')
>>>
>>> e.intro()
"Hi I'm Sangeeta! I'm on the tech team and I report to Luke Skywalker."

Now we’re really getting a sense of how inheritance offers a great deal of composability and code reuse. The ability to hook into parent methods gives us way better control of code execution.


Let’s look at a multiple inheritance example using super. In this example, we have a bunch of classes (A, B, C) that add a single letter to a list. The MoreLetters class extends that list with multiple letters.

class A:
    def __init__(self):
        self.letters = []

    def add_letter(self):
        self.letters.append('A')


class B:
    def __init__(self):
        self.letters = []

    def add_letter(self):
        self.letters.append('B')


class C(A):
    def add_letter(self):
        super().add_letter()
        self.letters.append('C')


class MoreLetters(C, B, A):
    def add_letter(self):
        super().add_letter()
        self.letters.extend(['D', 'E', 'F'])

The MoreLetters class inherits from C, B, and A in that order. Its add_letter method calls super, which calls C’s add_letter method. C’s add_letter method also calls super, which does not call A’s add_letter method, but instead calls the method from the next class in the MRO which is B!

>>> m = MoreLetters()
>>>
>>> MoreLetters.mro()
[<class '__main__.MoreLetters'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>]
>>>
>>> m.add_letter()
>>> print(m.letters)
['B', 'C', 'D', 'E', 'F']
>>>

Our Employee inheritance example 4

We actually never call class A’s add_letter because the furthest up we reach in the MRO is class B where the method execution stops.

If we wanted to call A’s add_letter, we can achieve this by adding a super call in class B’s add_letter method to then bubble up to class A’s add_letter method. Let’s add it in and see the results:

class A:
    def __init__(self):
        self.letters = []

    def add_letter(self):
        self.letters.append('A')


class B:
    def __init__(self):
        self.letters = []

    def add_letter(self):
        super().add_letter()  # new
        self.letters.append('B')


class C(A):
    def add_letter(self):
        super().add_letter()
        self.letters.append('C')


class MoreLetters(C, B, A):
    def add_letter(self):
        super().add_letter()
        self.letters.extend(['D', 'E', 'F'])

Now, we see the letter ‘A’ get appended to our list.

>>> m = MoreLetters()
>>> print(MoreLetters.mro())
[<class '__main__.MoreLetters'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>]
>>>
>>> m.add_letter()
>>> print(m.letters)
['A', 'B', 'C', 'D', 'E', 'F']
>>>

⚠️ Caution: Having the super call in class B’s add_letter method only works because of this specific MRO order (C, B, A). We know beforehand that B’s super call would resolve to A’s add_letter method.

On the other hand, if we were to create an instance of class B and call the add_letter, we would get an error because the object base class has no add_letter method:

>>> b = B()
>>>
>>> B.mro()
[<class '__main__.B'>, <class 'object'>]
>>>
>>> b.add_letter()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/sangeeta/Code/pystuff/super.py", line 57, in add_letter
    super().add_letter()
AttributeError: 'super' object has no attribute 'add_letter'

Actually, class B is a great example of a mixin because its a standalone class intended to add some additional functionality to other classes and not intended for use on its own.

Mixins 🍪

Multiple inheritance is a double edged sword, it can become confusing really fast and lead to unexpected results if not understood properly. A practice to avoid such pitfalls is the use of mixin classes implemented through multiple inheritance.

Mixins are classes that offer some specific functionality. There’s nothing special about them, they’re just regular classes, but they are intended to be small, not inheriting from any common parent classes other than object, and designed for the following reasons:

  1. Either to add a lot of optional features for a class
  2. Use a particular feature across many classes

Like we saw in the prior example, class B had a super call in its add_letter method even though it didn’t inherit from any other classes besides object. This is because we knew that in the MRO ordering, B’s super call would resolve to A’s add_letter method.

To summarize, in order for mixins to work effectively, they should always go to the left in a class definition so that they intercept the logic flow first. At this point, the mixin can either extend functionality by calling super or replace that logic entirely. Coincidentally, mixins aren’t meant to be used on their own and rather through multiple inheritance whereby at the time they’re invoked, they have access to other methods and attributes thanks to MRO.


Imagine we have a Cookie class which defines some procedural methods for baking cookies. Assume that the general code execution happens by chaining methods together.

Cookie example

However, we love chocolate so much that we want to make a chocolate chip cookie by leveraging our existing Cookie class! We can use alot of the functionality that our Cookie class offers but just hijack the mix_ingredients method to secretly add in some chocolate chips to the ingredients before mixing. To do so, we define a ChocolateChipMixin to mix in the chocolate chips (pun intended)! 😉

Finally, we define our ChocolateChipCookie class which inherits from our ChocolateChipMixin and Cookie classes in that order.

Cookie example

Yay! Now we can create tasty cookies and chocolate chip cookies thanks to mixins. To brainstorm a little further, imagine the many ways we can use our ChocolateChipMixin class. We can use it in other yummy classes as long as they also define a mix_ingredients method with the same signature; this can be anything like cakes, milkshakes, banana bread, ice-cream…the possibilities are endless!

Star Wars Mixins Edition

To illustrate another use of mixins, lets look at a Star Wars example for creating different types of Jedi and Sith instances.

We have our ForceUser class which describes someone who is force sensitive. Then we define a couple of mixins; the JediMixin and SithMixin both define custom lightsaber color and the LightsaberMixin uses the predefined lightsaber colors to create a lightsaber for our force user.

Finally we bring everything together by defining our JediMaster and SithLord classes. These two classes override some ForceUser methods to add their own custom logic.

import random


class ForceUser:
    def __init__(self, name):
        self.moves = ['force push']
        self.name = name

    def get_ability(self):
        return random.choice(self.moves)

    def ability(self):
        print(f'Attacks with {self.get_ability()}')

    def __repr__(self):
        return f'I am {self.name}, a {self.__class__.__name__}!'


######################################


class JediMixin:
    lightsaber_colors = ['blue', 'green', 'purple']


class SithMixin:
    lightsaber_colors = ['red']


class LightsaberMixin:
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.weapon = f'{self._get_lightsaber_color()} lightsaber'

    def _get_lightsaber_color(self):
        return random.choice(self.lightsaber_colors)

    def strike(self):
        print(f'Strikes with {self.weapon}')


######################################


class JediMaster(LightsaberMixin, JediMixin, ForceUser):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.moves.extend(['force healing', 'wall of light'])

    def ability(self):
        print('Only a Sith deals in absolutes! 👎🏼')
        super().ability()


class SithLord(SithMixin, LightsaberMixin, ForceUser):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.moves.extend(['force lightning', 'force shock'])

    def ability(self):
        print('UNLIMITED POWERRRR!!! ⚡️⚡️⚡️')
        super().ability()

See the Sith in action:

>>> sidious = SithLord('Darth Sidious')
>>>
>>> sidious
I am Darth Sidious, a SithLord!
>>>
>>> sidious.ability()
UNLIMITED POWERRRR!!! ⚡️⚡️⚡️
Attacks with force lightning
>>>
>>> sidious.ability()
UNLIMITED POWERRRR!!! ⚡️⚡️⚡️
Attacks with force push
>>>
>>> sidious.strike()
Strikes with red lightsaber
>>>
>>> sidious.moves
['force push', 'force lightning', 'force shock']

See the Jedi in action:

>>> obiwan
I am Obi-Wan Kenobi, a JediMaster!
>>>
>>> obiwan.ability()
Only a Sith deals in absolutes! 👎🏼
Attacks with force push
>>>
>>> obiwan.ability()
Only a Sith deals in absolutes! 👎🏼
Attacks with wall of light
>>>
>>> obiwan.strike()
Strikes with purple lightsaber
>>>
>>> obiwan.moves
['force push', 'force healing', 'wall of light']

One thing you might have noticed is that the order of the mixins in JediMaster and SithLord are not the same, but they function the same. That’s because the JediMixin and SithMixin classes define the lightsaber colors as class variables.

In inheritance, class variables are shared by all instances of a class, so when we created the instances of JediMaster and SithLord, the LightsaberMixin class already has access to the lightsaber_colors class variable.

Take this example:

class A:
    name = 'Darth Vader'

class B:
    pass

class C(B, A):
    def __init__(self):
        print(f'My name is {self.name}')

Any instance of class C can access the name class variable directly on itself.

>>> c = C()
My name is Darth Vader

Django Mixins Edition 💚

Now, lets see a real world example using mixins. The Django web framework uses a lot of classes and mixins to offer modular functionality you can use in your code – think of them like Lego blocks you can put together. The source code is a great place to take a peek under the hood to see how Django internals work.

Django provides two ways to write views; function based views and class based views. Class based views (CBV’s) provide a lot of default behavior out of the box so you don’t need to spend time re-implementing the same features over and over. We’ll be using a CBV in our example soon!

Hooking into Django CBV’s require some context beforehand. The website ccbv.co.uk is an amazing resource for looking up a CBV and deciding which method(s) to override. Here are a few helpful tips to keep in mind when hijacking class methods or writing mixins:

  1. Figure out data you want to modify and find a method that modifies or returns that data
  2. Keep the method signature the same
  3. Check if the method returns anything at all, and follow the same pattern
  4. If you want to extend functionality, call super first then add your custom logic
  5. If you want to replace functionality, add your custom logic and don’t call super 🙂

In the following example, we will be using Django’s TemplateView to render an HTML template and define a custom mixin to add a cookie to each page.

# views.py

from django.views.generic import TemplateView


class SpyCookieMixin:
    def render_to_response(self, context, **response_kwargs):
        response = super().render_to_response(context, **response_kwargs)

        if not self.request.COOKIES.get('spy-cookie'):
            path = self.request.path
            response.set_cookie(
                'spy-cookie', f"I'm watching you on {path}",
                path=path
            )
        return response


class HomePageView(SpyCookieMixin, TemplateView):
    template_name = 'home.html'


class AboutPageView(SpyCookieMixin, TemplateView):
    template_name = 'about.html'

Let’s walk through the approach using the tips outlined above.

The purpose of the SpyCookieMixin is to set cookies on the HTTP response object before it gets returned. The best method to get a hold of this is render_to_response method which returns the HTTP response object.

We create the SpyCookieMixin and override the render_to_response method with the same exact signature as TemplateView’s render_to_response method.

The render_to_response method returns the response, so that means when we override it, we need to return the response:

We want to extend functionality with our mixin, so we store the response returned from the super call first, then modify and return it.


Here’s an example of a mixin that replaces functionality instead of extending it:

# views.py

class SpyContextMixin:
    def get_context_data(self, **kwargs):
        context = {'spy-message': f"I'm watching you on {self.request.path}"}
        return context


class HomePageView(SpyContextMixin, TemplateView):
    template_name = 'home.html'

The purpose of the SpyContentMixin is to replace the template context entirely. The best method to get a hold of the context is the get_context_data method which returns the context dictionary.

We create the SpyContextMixin and override the get_context_data method with the same exact signature as TemplateView’s get_context_data method.

The get_context_data method returns the context dictionary, so we must return a context dictionary.

We want to replace functionality with our mixin, so we simply return our own context dictionary with whatever we want. According to the MRO, our mixin’s get_context_data method will be the only get_context_data that ever gets called in our HomePageView. Be careful however, replacing internal functionality assumes you know what you’re doing! The last thing we want to do is to erase some part of internal logic we need!

These two examples illustrate the flexibility of mixins through multiple inheritance in Django. This approach requires understanding of the execution flow, and always keep in mind to modify only what you need!


Yay, we’ve finally reached the end of this blog post! If you’re still here, thank you for reading! You deserve a cookie! 🍪

I hope these examples helped you better understand Python inheritance, multi-inheritance and its use cases. The order of things explained here is how I’d wish I approached inheritance many years ago.

Understanding inheritance, especially its use cases in real world applications, definitely takes time and practice. One of the most important things to take away is that inheritance always occurs in the context of the MRO; being aware of this will always help you define the big picture.

As much as I love inheritance and think that its a powerful tool, it can get quite confusing sometimes. When in doubt, use your discretion and remember that simple is better than complex…your future self will thank you!

As always, thanks for reading and happy coding! ✨