Learn Python / Inheritance

Inheritance

Often several kinds of things share most of their behaviour but differ in a few details. Cats and dogs are both animals; a savings account is a bank account with interest. Inheritance lets a new class start with everything another class has, then add or change only what's different.

A parent and a child class

The class being inherited from is the parent (or base class). The new class is the child (or subclass). Put the parent's name in brackets:

class Animal:
    def __init__(self, name):
        self.name = name

    def eat(self):
        print(f"{self.name} is eating.")

class Dog(Animal):
    def bark(self):
        print(f"{self.name} says Woof!")

rex = Dog("Rex")
rex.eat()     # inherited from Animal
rex.bark()    # defined in Dog
Output

Dog didn't define __init__ or eat, but it has them because it inherits them from Animal.

Overriding methods

A child class can replace a parent's method by defining one with the same name. This is called overriding:

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "..."

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"

for pet in [Dog("Rex"), Cat("Tom"), Animal("Generic")]:
    print(pet.name, "says", pet.speak())
Output

Python looks for a method on the object's own class first, then on its parent, then on the parent's parent, and so on.

Extending with super()

Often you don't want to replace the parent's method, but add to it. super() gives you access to the parent's version. It's most common in __init__:

class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)   # let Animal set up the name
        self.breed = breed       # then add what's new

rex = Dog("Rex", "Labrador")
print(rex.name, "-", rex.breed)
Output

If a child defines its own __init__ and forgets to call super().__init__(), the parent's setup never runs:

class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, name, breed):
        self.breed = breed       # forgot super().__init__(name)

rex = Dog("Rex", "Labrador")
print(rex.name)
Output

super() works in any method, not just __init__:

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def describe(self):
        return f"{self.owner}: {self.balance:.2f}"

class SavingsAccount(BankAccount):
    def __init__(self, owner, balance=0, rate=0.03):
        super().__init__(owner, balance)
        self.rate = rate

    def add_interest(self):
        self.balance += self.balance * self.rate

    def describe(self):
        return super().describe() + f" (savings at {self.rate:.0%})"

savings = SavingsAccount("Ada", 1000)
savings.add_interest()
print(savings.describe())
Output

Polymorphism

Because every animal has a speak() method, code can treat them all the same way without checking what kind each one is. This is called polymorphism, which means "many forms":

class Shape:
    def area(self):
        return 0

class Rectangle(Shape):
    def __init__(self, w, h):
        self.w, self.h = w, h

    def area(self):
        return self.w * self.h

class Circle(Shape):
    def __init__(self, r):
        self.r = r

    def area(self):
        return 3.14159 * self.r ** 2

shapes = [Rectangle(3, 4), Circle(1), Rectangle(2, 2)]

total = sum(shape.area() for shape in shapes)
print(f"Total area: {total:.2f}")
Output

Adding a new shape later, such as a Triangle, needs no change to the loop at all. Just give the new class an area() method.

Going deeper: Abstract base classes: forcing subclasses to fill in methods optional

In the example above, a new shape that forgets area() silently inherits return 0 and gives wrong totals. An abstract base class turns that mistake into an error straight away. Inherit from ABC and mark the methods every subclass must provide with @abstractmethod:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side ** 2

class Triangle(Shape):     # forgot to write area()
    pass

print(Square(3).area())
Triangle()                 # TypeError: can't create it without area()
Output

You can't create a Shape itself either, only subclasses that fill in every abstract method. It's a contract: "anything that calls itself a Shape must have an area()".

Checking types: isinstance and issubclass

isinstance() checks whether an object is an instance of a class or any of its subclasses:

class Animal:
    pass

class Dog(Animal):
    pass

rex = Dog()

print(isinstance(rex, Dog))
print(isinstance(rex, Animal))   # a Dog is also an Animal
print(isinstance(rex, str))
print(issubclass(Dog, Animal))
Output

Everything inherits from object

Every class in Python ultimately inherits from the built-in object class. That's where default behaviour, such as how an object prints, comes from. Even exceptions use inheritance: that's why the custom error in the Errors and Exceptions lesson inherited from Exception:

class Animal:
    pass

class Dog(Animal):
    pass

print(Dog.__mro__)    # the order Python searches for methods
print(issubclass(ValueError, Exception))
print(issubclass(ZeroDivisionError, ArithmeticError))
Output

__mro__ stands for method resolution order: the chain of classes Python searches, from the object's own class up to object.

When to use inheritance

Use inheritance for an "is a" relationship: a dog is an animal, a savings account is a bank account. If one thing merely has another, like a car that has an engine, store it as an attribute instead. Keep hierarchies shallow: one or two levels is usually plenty.

Exercises

Exercise 1: Vehicles

Create a Vehicle class with a wheels attribute and a describe() method that returns "This vehicle has N wheels". Then create Car and Bike subclasses that set wheels to 4 and 2.

# define Vehicle, Car and Bike here

print(Car().describe())
print(Bike().describe())
Output

Exercise 2: Employees

Complete the Manager class. It should inherit from Employee, take an extra bonus argument, and override pay() to return the salary plus the bonus. Use super().

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def pay(self):
        return self.salary

class Manager(Employee):
    pass

staff = [Employee("Leo", 3000), Manager("Mia", 4000, 500)]
for person in staff:
    print(person.name, person.pay())
Output

Exercise 3: Shapes with a square

Add a Square class that inherits from Rectangle. A square only needs one side length, so its __init__ should call super().__init__(side, side).

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

# define Square here

sq = Square(5)
print(sq.area())                  # 25
print(isinstance(sq, Rectangle))  # True
Output

Summary

  • class Child(Parent): creates a subclass that inherits all of the parent's attributes and methods.
  • Defining a method with the same name overrides the parent's version.
  • super() calls the parent's version. Use super().__init__(...) in a child's __init__.
  • Polymorphism: different classes share method names, so one piece of code can work with all of them.
  • isinstance(obj, Class) is also true for subclasses. Every class inherits from object.
  • Use inheritance for "is a" relationships, and attributes for "has a" relationships.