Inheritance

Languages that support object-oriented programming typically include the idea of inheritance to allow for greater code re-use and extensibility, and Python is one of those languages.

To see what we mean by inheritance, let's imagine that we're building a game, "Animal Conserving", that simulates an animal conservatory. We'll be taking care of both fuzzy and ferocious animals in this game, making sure they're well fed and cared for.

Pairs of animals playing and eating

We're going to use OOP to organize the data in the game. What do you think the classes should be?

Here's one approach:

# A class for meals
Meal()

# A class for each animal
Panda()
Lion()
Rabbit()
Vulture()
Elephant()

Let's start writing out the class definitions, starting simple with the Meal class:

class Meal:

    def __init__(self, name, kind, calories):
        self.name = name
        self.kind = kind
        self.calories = calories

Here's how we would construct a couple meals:

broccoli = Meal("Broccoli Rabe", "veggies", 20)
bone_marrow = Meal("Bone Marrow", "meat", 100)

Now, some of the animal classes, starting with our long-trunked friend: 🐘

class Elephant:
    species_name = "African Savanna Elephant"
    scientific_name = "Loxodonta africana"
    calories_needed = 8000

    def __init__(self, name, age=0):
        self.name = name
        self.age = age
        self.calories_eaten  = 0
        self.happiness = 0

    def eat(self, meal):
        self.calories_eaten += meal.calories
        print(f"Om nom nom yummy {meal.name}")
        if self.calories_eaten > self.calories_needed:
            self.happiness -= 1
            print("Ugh so full")

    def play(self, num_hours):
        self.happiness += (num_hours * 4)
        print("WHEEE PLAY TIME!")

    def interact_with(self, animal2):
        self.happiness += 1
        print(f"Yay happy fun time with {animal2.name}")

Every elephant shares a few class variables, species_name, scientific_name, and calories_needed. They each have their own name, age, calories_eaten, and happiness instance variables, however.

Let's make a playful pair of elephants:

el1 = Elephant("Willaby", 5)
el2 = Elephant("Wallaby", 3)
el1.play(2)
el1.interact_with(el2)

Next, let's write a class for our cute fuzzy long-eared friends: 🐇

class Rabbit:
    species_name = "European rabbit"
    scientific_name = "Oryctolagus cuniculus"
    calories_needed = 200

    def __init__(self, name, age=0):
        self.name = name
        self.age = age
        self.calories_eaten = 0
        self.happiness = 0

    def play(self, num_hours):
        self.happiness += (num_hours * 10)
        print("WHEEE PLAY TIME!")

    def eat(self, food):
        self.calories_eaten += food.calories
        print(f"Om nom nom yummy {food.name}")
        if self.calories_eaten > self.calories_needed:
            self.happiness -= 1
            print("Ugh so full")

    def interact_with(self, animal2):
        self.happiness += 4
        print(f"Yay happy fun time with {animal2.name}")

And construct a few famous rabbits:

rabbit1 = Rabbit("Mister Wabbit", 3)
rabbit2 = Rabbit("Bugs Bunny", 2)
rabbit1.eat(broccoli)
rabbit2.interact_with(rabbit1)

Do you notice similarities between the two animal classes? The structure of the two classes have much in common:

So it appears that 90% of their code is in fact the same. That violates a popular programming principle, "DRY" (Don't Repeat Yourself), and personally, makes my nose crinkle a little in disgust. Repeated code is generally a bad thing because we need to remember to update that code in multiple places, and we're liable to forget to keep code in sync that's meant to be the same.

Fortunately, we can use inheritance to rewrite this code. Instead of repeating the code, Elephant and Rabbit can inherit the code from a base class.

Base class

When multiple classes share similar attributes, you can reduce redundant code by defining a base class and then subclasses can inherit from the base class.

For example, we can first write an Animal base class, put all the common code in there, and the specific animal species can be subclasses of that base class:

A tree diagram with Animal at the top and specific animal classes as nodes underneath it

You'll also hear the base class referred to as the superclass.

Here's how we could write the Animal base class:

class Animal:
    species_name = "Animal"
    scientific_name = "Animalia"
    play_multiplier = 2
    interact_increment = 1

    def __init__(self, name, age=0):
        self.name = name
        self.age = age
        self.calories_eaten  = 0
        self.happiness = 0

    def play(self, num_hours):
        self.happiness += (num_hours * self.play_multiplier)
        print("WHEEE PLAY TIME!")

    def eat(self, food):
        self.calories_eaten += food.calories
        print(f"Om nom nom yummy {food.name}")
        if self.calories_eaten > self.calories_needed:
            self.happiness -= 1
            print("Ugh so full")

    def interact_with(self, animal2):
        self.happiness += self.interact_increment
        print(f"Yay happy fun time with {animal2.name}")

We even defined class variables in there. We didn't need to do that, since the values of those variables don't make sense, but it is helpful to show the recommended class variables for the subclasses.

Subclasses

To declare a subclass, put parentheses after the class name and specify the base class in the parentheses:

class Elephant(Animal):

Then the subclasses only need the code that's unique to them. They can redefine any aspect: class variables, method definitions, or constructor. A redefinition is called overriding.

Here's the full Elephant subclass, which only overrides the class variables:

class Elephant(Animal):
    species_name = "African Savanna Elephant"
    scientific_name = "Loxodonta africana"
    calories_needed = 8000
    play_multiplier = 4
    interact_increment = 1
    num_tusks = 2

Same for the Rabbit class:

class Rabbit(Animal):
    species_name = "European rabbit"
    scientific_name = "Oryctolagus cuniculus"
    calories_needed = 200
    play_multiplier = 10
    interact_increment = 2
    num_in_litter = 12

Overriding methods

A subclass can also override the methods of the base class. Python will always look for the method definition on the current object's class first, and will only look in the base class if it can't find it there.

We could override interact_with for pandas, since they're quite solitary creatures:

class Panda(Animal):
    species_name = "Giant Panda"
    scientific_name = "Ailuropoda melanoleuca"
    calories_needed = 6000

    def interact_with(self, other):
        print(f"I'm a Panda, I'm solitary, go away {other.name}!")

This code will call that overridden method definition instead of the Animal definition:

panda1 = Panda("Pandeybear", 6)
panda2 = Panda("Spot", 3)
panda1.interact_with(panda2)

The following code would not, however. Do you see why?

pandey = Panda("Pandeybear", 6)
bugs = Rabbit("Bugs Bunny", 2)
bugs.interact_with(pandey)

The object on the left-hand side of the dot notation is of type Rabbit and there is no interact_with defined on Rabbit, so the original Animal method definition will be used instead.

➡️ Next up: Exercise: Inheritance