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:
- the basics of Python inheritance - only what you need to know 👨👩👧👦
- multiple inheritance - setting the stage 🥞
- what even is MRO - the missing piece of the puzzle 😳
- super() to the rescue - bringing flexibility to inheritance 💥
- using mixins - multi-inheritance minus the confusion 🍪
- mixins example - Star Wars ⚡️
- mixins example - Django 💚
Inheritance 101 👨👩👧👦
Subclasses inherit all attributes and methods from a parent class if they don’t override them directly.
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’.
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.
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.
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.
Our Employee class uses its own name
attribute, not the Person’s name
attribute.
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:
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!
💡 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:
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:
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:
In this case, both Person and Pythonista inherit from the same parent class, so the MRO for Employee would be formed like this:
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:
If we try to create an Employee instance, we get hit with this:
With the MRO rule that children precede parents, you’d think the MRO would resolve itself but we run into some problems:
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
- use parent class methods
- or override methods if we want custom subclass logic
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.
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.
💡 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.
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.
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.
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!
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:
Now, we see the letter ‘A’ get appended to our list.
⚠️ 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:
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:
- Either to add a lot of optional features for a class
- 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.
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.
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.
See the Sith in action:
See the Jedi in action:
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:
Any instance of class C can access the name
class variable directly on itself.
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:
- Figure out data you want to modify and find a method that modifies or returns that data
- Keep the method signature the same
- Check if the method returns anything at all, and follow the same pattern
- If you want to extend functionality, call super first then add your custom logic
- 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.
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:
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! ✨