More on inheritance

Using super()

Sometimes we want to be able to call the original method definition from inside the overridden method definition. That is possible using super(), a function that can delegate method calls to the parent class.

For example, this Lion class has an eat method that only calls the original eat if the meal is meat:

class Lion(Animal):
    species_name = "Lion"
    scientific_name = "Panthera"
    calories_needed = 3000

    def eat(self, meal):
        if meal.kind == "meat":
            super().eat(meal)

Since Lion inherits from Animal, that line of code will call the definition of eat inside Animal instead, but pass in the Lion object as the self.

We'd call it the same way as we usually would:

bones = Meal("Bones", "meat")
mufasa = Lion("Mufasa", 10)
mufasa.eat(bones)

We could have also written this code instead to achieve the same result:

def eat(self, food):
    if food.kind == "meat":
        Animal.eat(self, food)

However, the great thing about super() is that it both keeps track of the parent class and takes care of passing in self. Super convenient!

Using super() with __init__

A very common use of super() is to call the __init__. Many times, a subclass wants to initialize all the original instance variables of the base class, but then additionally wants to set a few instance variables specific to its needs.

For example, consider this Elephant class:

class Elephant(Animal):
    species_name = "Elephant"
    scientific_name = "Loxodonta"
    calories_needed = 8000

    def __init__(self, name, age=0):
        super().__init__(name, age)
        if age < 1:
            self.calories_needed = 1000
        elif age < 5:
            self.calories_needed = 3000

That __init__ method first calls the original __init__ from Animal and then it conditionally overrides the calories_needed instance variable in the case of young elephants.

We can create an Elephant object in the usual manner:

elly = Elephant("Ellie", 3)
print(elly.calories_needed)

🧠 Check Your Understanding

What value would that code display?

Layers of inheritance

Every Python 3 class actually implicitly inherits from the global built-in object class.

Tree diagram with object class at the top, then Animal, then specific animal subclasses

We could have written our Animal class to explicitly inherit from it, like so:

class Animal(object):

But Python 3 assumes that any class without a base class inherits from object, so we usually don't bother with the extra code.

The object class includes a few default methods that we can use on any class, including a default implementation of __init__ that doesn't do much of anything at all - but it does mean that we can write a class without __init__ and won't run into errors when creating new instances.

We can also add more layers of inheritance ourselves, if we realize it would be sensible to have a deeper hierarchy of classes.

Tree diagram with object class, Animal class, then Herbivore and Carnivore class, with animal subclasses under them

To add that new layer of classes, we just define the new classes:

class Herbivore(Animal):

    def eat(self, meal):
        if meal.kind == "meat":
            self.happiness -= 5
        else:
            super().eat(meal)

class Carnivore(Animal):

    def eat(self, meal):
        if meal.kind == "meat":
            super().eat(meal)

Notice that these subclasses only override a single method, eat.

Then we change the classes that are currently inheriting from Animal to instead inherit from either Herbivore or Carnivore:

class Rabbit(Herbivore):
class Panda(Herbivore):
class Elephant(Herbivore):

class Vulture(Carnivore):
class Lion(Carnivore):

For a class that represents an omnivorous animal, we could keep its parent class as Animal and not change it to one of the new subclasses.

Multiple inheritance

This is where things start to get a little wild. A class can actually inherit from multiple other classes in Python.

For example, if Meal has a subclass of Prey (since, in the circle of life, animals can become meals!), an animal like Rabbit could inherit from both Prey and Herbivore. Poor rabbits!

Here's the Prey class:

class Prey(Meal):
    kind = "meat"
    calories = 200

To inherit from that class as well, we add it to the class names inside the parentheses:

class Rabbit(Herbivore, Prey):

We could even add another subclass to Animal to represent predators, making it so that interacting with a prey animal instantly turns into mealtime.

class Predator(Animal):

    def interact_with(self, other):
        if other.kind == "meat":
            self.eat(other)
            print("om nom nom, I'm a predator")
        else:
            super().interact_with(other)

We once again would need to modify the class names in the parentheses to make a subclass inherit from Predator as well:

class Lion(Carnivore, Predator):

Python can look for attributes in any of the parent classes. It first looks in the current class, then looks at the parent class, then looks at that class's parent class, etc.

r = Rabbit("Peter", 4)           # Animal __init__
r.play()                         # Animal method
r.kind                           # Prey class variable
r.eat(Food("carrot", "veggies")) # Herbivore method
l = Lion("Scar", 12)             # Animal __init__
l.eat(Food("zazu", "meat"))      # Carnivore method
l.interact_with(r)               # Predator method

Actually, the way that it finds attributes is even more complicated than that, since it will also look in sibling classes. We won't dive into that here, but if you're curious, search the web for "Python method resolution order (MRO").

Too much inheritance?

Inheritance is a great way to avoid repeated code and express the relationships between objects in your programs. However, be careful not to over-use it or take it too far. It can lead to confusing code with surprising results when there are multiple places overriding the same method.

A good strategy is to start with as little inheritance as possible, perhaps with a single base class (besides object), and then only add more levels of inheritance when it's becoming painful not to add them.

Another good idea is to use a "mix-in" approach to multiple inheritance: additional base classes should only add new methods, not override methods. That way, you don't have to worry about the order in which Python will look for methods on the base classes.

➡️ Next up: Exercise: More on inheritance