Join us and get access to thousands of tutorials and a community of expert Pythonistas.
This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.
Multiple Inheritance
00:00 Up until now, we’ve designed class hierarchies where each class inherits from at most one other class. This is called single inheritance, but we can go further than that.
00:13 Python is one of the few popular languages that supports this idea of multiple inheritance. Multiple inheritance allows one class to derive from, or inherit from, multiple other classes instead of just one.
00:30
But where would this be useful? Well, take this scenario. We want to add a new class to our employee tracking system that represents a temporary secretary. If you remember, the Secretary class is a SalaryEmployee.
00:47
This temporary employee would be different in that it would be treated like a normal Secretary to the ProductivitySystem, but it would be paid like an HourlyEmployee.
00:58
We could achieve this with single inheritance—for example, by inheriting from Secretary—so it’s tracked using the inherited .work() method—and then creating our own .calculate_payroll() method to calculate payroll as an hourly employee.
01:15
But what’s the fun in that? Let’s try inheriting from both classes and just see what happens. I’m here in Visual Studio Code in employees.py.
01:27 We want to create a new class that represents a temporary secretary, so let’s move to the bottom of this file and create that class.
01:40 To inherit from multiple classes, separate the class names with a comma, just like this.
01:48
For now, I’m just going to create an empty class so that it uses its parents’ .__init__() method for object construction. Let’s try this out!
01:58
I’m going to move over to program.py and right underneath the factory_worker, I’m going to create a new object for this temporary secretary.
02:10
I’ll give them an id and a name,
02:15
and because they’re going to be billed hourly, they’ll need to have an .hours_worked and .hour_rate attribute.
02:27
Let’s run this, and I will finish off this quick video. Ha. Yeah, quick videos aren’t really a thing in this course. We got an exception that says .__init__() takes four arguments, but we gave it five.
02:41
This error can be a bit confusing because it includes the self argument, which we don’t actually pass into the .__init__() method ourselves, so just subtract one from each. .__init__() takes three arguments, but we gave it four. That makes sense!
02:58
We supplied four arguments when creating our new object, but what .__init__() method is it even checking against? In theory, we just inherited two, right? Well, if we move back into employees.py, notice that I inherited from Secretary and then HourlyEmployee.
03:19
It looks like it’s trying to use the Secretary’s .__init__() method, which takes only a weekly_salary, instead of the two arguments required by the HourlyEmployee. Let’s switch these and see what happens.
03:40
Okay. Now, it says that we’re missing the weekly_salary parameter. It’s checking against a different .__init__() method now—the one for the SalaryEmployee.
03:51 This seems kind of unpredictable, but luckily for us, there’s a way to see what it’s doing. It’s called the MRO, or method resolution order. Formally speaking, the MRO is a set of rules that defines the search path that Python will use when searching for the right method to use in cases of inheritance.
04:15 This search path is like an ordered list of classes, and every class has its own MRO. In this video, you’ll see how it’s used to demystify multiple inheritance, but it’s actually used in single inheritance too—although, then, it’s kind of trivial.
04:34
You’ll also see how this list of classes is used by the super() function. We can view the MRO of any class with its special .__mro__ attribute. To do that,
04:46
I’m going to open a new interactive shell, I’ll grab the class with from employees import TemporarySecretary, and now I’ll write TemporarySecretary.__mro__ (dunder mro).
05:03
And when I press Enter, you see we get a listing of classes. This is the method resolution order for the TemporarySecretary class. When we call any method on TemporarySecretary, including .__init__(), this is the order in which it will be searched for.
05:23
It looks like the .__init__() method is being searched for in the following order: TemporarySecretary, HourlyEmployee, Secretary, SalaryEmployee, and finally, Employee.
05:37
As soon as Python finds an .__init__() method in one of these classes that matches the arguments we’ve passed in—aka the method’s signature—it will use that .__init__() method.
05:49 The problem is it’s not finding one, and so it’s giving us an exception.
05:55 Let me explain what’s going on under the hood.
05:59
TemporarySecretary defines no .__init__() method itself, so it needs to use one that it inherits. The MRO says that searching order should always be left-to-right, children before parents, and so when it can’t find an .__init__() method inside of TemporarySecretary, Python searches HourlyEmployee next.
06:22
If we look at the definition for HourlyEmployee, it looks like it has a matching signature for the .__init__() method, but that method calls super(), which tells the MRO to keep searching the list starting with the next class.
06:39
A common misconception is that super() calls the parent of the current class—in this case, Employee. That’s simply not true. It just calls the next class in the current MRO list.
06:53
In this case, the next class it looks in is Secretary, but if we look at that class, it looks like Secretary doesn’t define an .__init__() method, so it searches its parent, SalaryEmployee. Here, the method signature for the .__init__() method doesn’t match what we’ve passed in.
07:13
It takes three arguments, but we’ve supplied four—or really, four and five, if you include self. And that is why we are getting this exception. In order to bypass the MRO,
07:28
I’m first going to switch the order of inheritance for TemporarySecretary.
07:35
Now, I’m going to create an .__init__() method that accepts all of the arguments it needs to be instantiated as an HourlyEmployee. In essence, by overriding the .__init__() method of the parents and not using the super() function inside it, we are bypassing the MRO. Instead of super(), I’ll call the parent class I want directly, which looks like this: HourlyEmployee.__init__().
08:08
Now, the TemporarySecretary will be instantiated using the HourlyEmployee constructor, but it will still work like a Secretary since it still inherits that method from the Secretary class.
08:22
Our object will be instantiated fine, but when the PayrollSystem tries to call .calculate_payroll() on a TemporarySecretary object, we’re going to get an exception telling us that we haven’t supplied a weekly_salary.
08:37
That is because TemporarySecretary doesn’t define a .calculate_payroll() method, so naturally, Python searches its parents according to the MRO. The next class is SalaryEmployee, which defines a .calculate_payroll() method, but it requires a .weekly_salary attribute and we don’t have that with this TemporarySecretary since we only initialized it with .hours_worked and .hour_rate attributes.
09:08
Luckily, this is a quick fix. All I’m going to do is define a .calculate_payroll() method in the TemporarySecretary class. And when that is called, I’ll tell it to return the HourlyEmployee’s .calculate_payroll() method.
09:26
Let’s move over to program.py and see how this works.
09:32
Great. Our TemporarySecretary is working like a Secretary and being paid like an HourlyEmployee. Thank goodness that’s over.
09:42 What you just witnessed is called the diamond problem. The diamond problem occurs when a class inherits from two or more classes, each of which inherits from a single common ancestor.
09:57
When this happens, the method resolution order is used to determine what order to search parent classes in. But, as you saw, this can get pretty messy and the only way we could fix it was with a sort of band-aid patch on our TemporarySecretary class—and even then, we had to be careful. When you see a diamond, it’s typically time to rethink the design of your software.
10:25 Ideally, if you plan out as much of your project as you can ahead of time, you can avoid this problem altogether. The next two videos are going to cover two fundamental questions.
10:38 The first one is “How does Python determine the method resolution order?” and the second is “How do we redesign our project to utilize multiple inheritance, but without the diamond problem?” The next video regarding the MRO is optional.
10:56 It’s not very important that you understand the algorithm behind it but I think it’s pretty cool, so I included it as sort of a bonus. If you’re not interested in that, you can skip to the following video in the course, where I show you how to redesign this project.
agupt039 on March 20, 2021
09:08 Luckily, this is a quick fix. All I’m going to do is define a .calculate_payroll() method in the TemporarySecretary class. And when that is called, I’ll tell it to return the HourlyEmployee’s .calculate_payroll() method.
While calling HourlyEmployee’s init method, why self is used, on the other hand, I do not see self with super()?
super confusing
Bartosz Zaczyński RP Team on March 22, 2021
@agupt039 When you call a method on an object, then Python will implicitly pass the reference to that object as the first argument, which is customarily called self. However, you can also call that same method through the class name and pass the reference to a specific object yourself:
secretary = TemporarySecretary(42, "Anna", 80, 7.25)
# Method bound to the "secretary" instance:
secretary.calculate_payroll()
# Unbound class method:
HourlyEmployee.calculate_payroll(secretary)
The method in the first call is bound to the secretary instance, so Python knows which object to call it upon. The other one is unbound, so you need to provide an object yourself.
The latter way of calling a method can be helpful in the context of inheritance. For example, it lets you delegate a call to another method specified in one of your base classes, like in the video:
def calculate_payroll(self):
HourlyEmployee.calculate_payroll(self)
That was the only way of doing delegation until the super() function was introduced in Python 2.2. However, the original syntax of the function was a little repetitive because it required both the class and instance parameters to be passed:
class TemporarySecretary(...):
def calculate_payroll(self):
super(TemporarySecretary, self).calculate_payroll()
Note that you had to pass the child class instead of the parent one! While you can still use this syntax, the current one lets you write the same without repeating yourself:
class TemporarySecretary(...):
def calculate_payroll(self):
super().calculate_payroll()
However, there is a subtle difference between calling super() or super(cls, self) and through a base class name like before. It manifests itself when you’re dealing with multiple inheritance, i.e., when your child class has more than one parent. In such a case, there could potentially be a conflict between identically named methods in your base classes. How would Python know which one to call if you used super() without pinpointing an exact parent class?
To avoid this ambiguity, which is known as the diamond inheritance problem, you can rely on the old syntax. But, it turns out there’s an algorithm called C3 linearization that helps super() sort this out. You can also inspect or even change the default order of your base classes using method resolution order (MRO).
alazejha on May 25, 2021
Hi, just a quck question. Is it possible to trace MRO from Jupyter Notebook? Thanks!
Bartosz Zaczyński RP Team on May 26, 2021
@alazejha I don’t see why not. After all, it’s just a class attribute:
>>> class Dad:
... pass
...
>>> class Mom:
... pass
...
>>> class Child(Mom, Dad):
... pass
...
>>> Child.__mro__
(<class '__main__.Child'>, <class '__main__.Mom'>, <class '__main__.Dad'>, <class 'object'>)
On the other hand, if you wanted to introspect the MRO based on an instance, then just do this:
>>> instance = Child()
>>> instance.__class__.__mro__
(<class '__main__.Child'>, <class '__main__.Mom'>, <class '__main__.Dad'>, <class 'object'>)
Chase on June 30, 2021
Why do we need to both call HourlyEmployee.__init__ AND the def __init__ constructor on TemporarySecretary? Doesn’t the init method override it already?
class TemporarySecretary(Secretary, HourlyEmployee):
def __init__(self, id, name, hours_worked, hour_rate):
HourlyEmployee.__init__(self, id, name, hours_worked, hour_rate)
Bartosz Zaczyński RP Team on July 1, 2021
@chasemthomas TL;DR It has to do with multiple inheritance. The two parent classes have incompatible initializer signatures, which is why you must intervene by providing one of your own.
When you implement a custom .__init__() method in your subclass, you effectively take control over the object construction process. In most cases, you’ll just call super() to rely on the method resolution order (MRO), which determines your parent class. Alternatively, you can cherry-pick your superclass’ initializers and call them manually, which used to be the old-school way of doing inheritance in Python. Today, it usually indicates that your type hierarchy doesn’t reflect the reality anymore, hence the need for a little bit of hacking. You can avoid such problems by limiting the use of inheritance in favor of composition.
saurabhjhingan08 on Dec. 27, 2021
Thanks @Saul for clearly explaining and clearing out that doubt. I had the same question as the one raised by @dwalsh.
damipatira on Jan. 13, 2022
Thank @Bartosz Zaczyńsk had the same question for self. Well explained, Much appreciated.
Dondi on May 10, 2023
Thanks so much @Saul!
Become a Member to join the conversation.
Saul on Nov. 9, 2020
I want to try to answer dwalsh’s question slowly and methodically. Hopefully I will not go astray in the process.
First let’s repeat the beginning of the class declaration in this particular inheritance configuration:
Then let’s show the instantiation of that class:
And as a final preparation for discussion, let’s outline the MRO for TemporarySecretary in this particular inheritance configuration:
When we search for an
__init__method, it is important to realize that Python isn’t going to jump over an__init__method in the MRO chain that doesn’t match the calling signature. Python looks for the first__init__it can find. As soon as Python finds an__init__, it compares the signatures between the caller and the method. If those signatures don’t match, an exception will be thrown immediately. No further searching along the MRO chain takes place.With this in mind, when our initial search occurs for an
__init__method, we first look in TemporarySecretary, and as you noted, because no__init__was found, we moved along the MRO chain to look in HourlyEmployee. Here we find an__init__method that also happens to match our calling signature:It’s important to emphasize that we stop searching for an
__init__method here regardless of whether the signature matches. If the signature had not matched, Python would have immediately thrown an exception and halted execution. But because it does match, we go on to execute that__init__method with the corresponding arguments.In the process of executing that method, we immediately come to a new call to super:
super().__init__(id, name)This call commences a new search for an
__init__method. The signature that we will attempt to match against is also new. We now need an init with only 2 positional arguments (or 3 if you count the implicit self argument). But once again, as soon as any__init__is found, the search stops. If the signature matches, we proceed to execute. If it does not, an exception is thrown.It is also important to note here that we will follow the original TemporarySecretary class MRO in our new search for an
__init__method. Successive calls to super follow the original MRO. Because we are currently in HourlyEmployee, the next class in the chain is Secretary. As you noted, Secretary has no__init__. So we then move along to SalaryEmployee, which looks like the following:SalaryEmployee has an
__init__but its signature does not match the new caller. The caller supplies id and name, but the SalaryEmployee also requires weekly_salary. In this case, we have too few arguments. Once again, Python stops searching as soon as it finds an__init__of any signature. Because the signatures do not match, an exception is immediately thrown:TypeError:
__init__() missing 1 required positional argument: ‘weekly_salary’We never reach Employee or object in our search for an
__init__method.