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 DogDog 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())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)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)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())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}")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()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))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))__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())
Give Vehicle an __init__(self, wheels) and a describe method that uses self.wheels.
In Car and Bike, write __init__(self) and call super().__init__(4) or super().__init__(2).
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())Manager.__init__ takes name, salary, bonus and calls super().__init__(name, salary) first.
Override pay so it returns super().pay() + self.bonus.
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)) # TrueWrite class Square(Rectangle): so it inherits area().
Its __init__(self, side) only needs to call super().__init__(side, side).
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. Usesuper().__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 fromobject.- Use inheritance for "is a" relationships, and attributes for "has a" relationships.