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.
Zarata on April 15, 2020
Java avoids large bottles of Tylenol by prohibiting multiple inheritance, though does allow “implements” of multiple interfaces. So, the search for a method goes linearly through an explicit single chain of child-parent inheritance and method overrides, or quickly ends at an interface method you’ve explicitly contracted to “implement(s)” in your class (though maybe might be a class you intend to parent … I think Austin mentioned the “abstract” concept back there somewhere). I’m not expert, but I think that’s more or less right … and probably same or similar in C# (I’ve only done a little C#, and long ago).
dwalsh on May 27, 2020
I needed to watch this video like 5 times and type out the following to understand something about the super() function.
When you called TemporarySecretary(HourlyEmployee,Secretary) at around 3:40 you state the init is being searched for via the following mro path to match the 4 positional arguments passed in:
- TemporarySecretary (no
__init__
so move to HourlyEmployee) - HourlyEmployee (a match with the parameters but because super() is called, it will skip to the next in the mro which is Secretary not the Parent of HourlyEmployee which is Employee)
- Secretary (no
__init__
so move to SalaryEmployee) - SalaryEmployee (
__init__
method doesn’t match the 4 positional argument as it has 3). IT CALLS THE SUPER() SO IT WILL MOVE TO EMPLOYEE - Employee (
__init__
method doesn’t match the 4 positional arguments as it has 3 so it would move to object to check). - Object (Object doesn’t have 4 positional arguments so it fails).
I just want to confirm that super() is called at the SalaryEmployee level like at the HourlyEmployee level and it’s checking the Employee level but it’s init doesn’t match so it fails and checks the parent object.
dwalsh on May 27, 2020
This is great material by the way and well presented. It’s just taking me multiple goes to wrap my head around the intricacies. Anyone else…just stick with this! It makes sense.
ayushm796 on Aug. 26, 2020
TemporarySecretary() calls –> HourlyEmployee’s init() which matches our TemporarySecretary’s init() with 4 arguments.
HourlyEmployee’s init() calls –> super().__init__()
–> which in turn invokes Secretary class’ init() and since our Secretary class’ init() just takes a single argument(and we passed 4) thereby causing a TypeError
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.
When the author called TemporarySecretary(HourlyEmployee,Secretary) at around 3:40 you state the init is being searched for via the following mro path to match the 4 positional arguments passed in …
First let’s repeat the beginning of the class declaration in this particular inheritance configuration:
class TemporarySecretary(HourlyEmployee, Secretary):
Then let’s show the instantiation of that class:
temporary_secretary = employees.TemporarySecretary(5, 'Robin Williams', 40, 9)
And as a final preparation for discussion, let’s outline the MRO for TemporarySecretary in this particular inheritance configuration:
- <class ‘employees.TemporarySecretary’>
- <class ‘employees.HourlyEmployee’>
- <class ‘employees.Secretary’>
- <class ‘employees.SalaryEmployee’>
- <class ‘employees.Employee’>
- <class ‘object’>
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:
def __init__(self, id, name, hours_worked, hour_rate):
super().__init__(id, name)
self.hours_worked = hours_worked
self.hour_rate = hour_rate
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:
class SalaryEmployee(Employee):
def __init__(self, id, name, weekly_salary):
super().__init__(id, name)
self.weekly_salary = weekly_salary
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.
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'>)
Mark 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.
Lucas Dondo on May 10, 2023
Thanks so much @Saul!
Become a Member to join the conversation.
bvdburg on April 15, 2020
Which languages support MRO and which ones don’t? I am especially interested in python vs c# vs java vs php.