Inheritance and Polymorphism
Building new classes from existing ones, overriding behaviour, and why a single loop can correctly handle a dozen different kinds of object.
Inheritance
Inheritance lets a new class take on all the attributes and methods of an existing one, then add or change what it needs.
The existing class is the parent, superclass or base class. The new one is the child, subclass or derived class.
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
def speak(self):
return "Some sound"
def describe(self):
return f"{self.name} is {self.age} years old"
class Dog(Animal):
def speak(self):
return "Woof"
class Cat(Animal):
def __init__(self, name, age, indoor=True):
super().__init__(name, age) # run the parent constructor
self.indoor = indoor
def speak(self):
return "Meow"
def describe(self):
base = super().describe() # reuse the parent version
where = "indoor" if self.indoor else "outdoor"
return f"{base} and is an {where} cat"
rex = Dog("Rex", 3)
tom = Cat("Tom", 5, indoor=False)
print(rex.describe()) # Rex is 3 years old
print(rex.speak()) # Woof
print(tom.describe()) # Tom is 5 years old and is an outdoor catWhat is happening
class Dog(Animal)means Dog inherits everything from Animal- Dog does not define
describe, so it uses Animal's - Dog defines its own
speak, which overrides the parent version super()calls the parent's version of a method, which lets you extend rather than replace
Shared behaviour is written once in the parent. Every subclass gets it automatically, and a fix in the parent fixes it everywhere at once.
- The parent class holds what all animals share.
- Dog inherits everything and overrides speak.
- Cat adds an attribute of its own and overrides speak and describe.
- Inherited lines are used as they are, overridden lines replace the parent's version, and new lines exist only in the subclass.
Polymorphism
Polymorphism means objects of different classes can be used through the same interface, each responding in its own way.
animals = [Dog("Rex", 3), Cat("Tom", 5), Dog("Bella", 1)]
for animal in animals:
print(f"{animal.name}: {animal.speak()}")The loop never checks what kind of animal it has. It calls speak() and each object runs its own version. Adding a Horse class later requires no change to this loop at all.
Why this matters enormously
Without polymorphism the loop would need:
for animal in animals:
if isinstance(animal, Dog):
print("Woof")
elif isinstance(animal, Cat):
print("Meow")
# and another branch for every new animal, foreverEvery new type of animal would mean editing every loop in the program. With polymorphism, new behaviour is added by writing a new class and nothing else changes.
An abstract base class
Sometimes the parent should never be instantiated on its own, only inherited from.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
def describe(self):
return f"{type(self).__name__} with area {self.area():.2f}"
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
for shape in [Circle(3), Square(4)]:
print(shape.describe())Shape guarantees that every subclass provides an area method. Trying to create a Shape() directly raises an error, which is exactly right because a shape with no specific form has no area.
- The abstract class says every shape has an area method, and the loop calls it on each shape.
- One call, shape.area(), is made, and each object answers with its own version.
- A Circle works out its area with its own method.
- A Square uses its own method. The loop never checks the type, so new shapes can be added without changing it.
Designing with classes
Composition, an alternative to inheritance
Inheritance means is a. Composition means has a.
class Engine:
def start(self):
return "Engine running"
class Car:
def __init__(self, make):
self.make = make
self.engine = Engine() # a Car HAS an Engine
def start(self):
return f"{self.make}: {self.engine.start()}"A Car is not a kind of Engine, so inheritance would be wrong here. It has one, so composition is right.
If you can say "a Dog is an Animal", inheritance fits. If you can only say "a Car has an Engine", use composition. Getting this wrong produces class hierarchies nobody can understand.
A worked example: a game
class Entity:
def __init__(self, name, x, y, health):
self.name = name
self.x = x
self.y = y
self.health = health
@property
def alive(self):
return self.health > 0
def take_damage(self, amount):
self.health = max(0, self.health - amount)
def update(self):
pass
class Player(Entity):
def __init__(self, name, x, y):
super().__init__(name, x, y, health=100)
self.score = 0
def update(self):
self.score += 1
class Enemy(Entity):
def __init__(self, name, x, y, damage):
super().__init__(name, x, y, health=30)
self.damage = damage
def update(self):
self.x += 1 # patrol
entities = [Player("Aisha", 0, 0), Enemy("Slime", 10, 0, damage=5), Enemy("Bat", 20, 5, damage=3)]
for step in range(3):
for e in entities:
e.update()
entities = [e for e in entities if e.alive]
for e in entities:
print(f"{e.name} at ({e.x}, {e.y}) with {e.health} health")One loop updates everything, whatever it is. That is the pattern behind every game engine you have ever used.
The four pillars, summarised
| Pillar | Meaning |
|---|---|
| Abstraction | Exposing only what is needed and hiding the complexity behind it |
| Encapsulation | Bundling data with its methods and controlling access to it |
| Inheritance | A class taking on the attributes and methods of another |
| Polymorphism | Objects of different classes responding to the same method call in their own way |
Key terms you must be able to define
- Inheritance
- A class taking on the attributes and methods of an existing class.
- Superclass
- The parent class that another class inherits from.
- Subclass
- A class that inherits from another class.
- Override
- Redefining a parent's method in a subclass so the subclass version is used.
- super()
- A function that calls the parent class's version of a method.
- Polymorphism
- Objects of different classes responding to the same method call in their own way.
- Abstract class
- A class that cannot be instantiated and exists only to be inherited from.
- Composition
- Building a class by giving it other objects as attributes, expressing a has a relationship.
- Abstraction
- Exposing only what is needed and hiding the underlying complexity.
Flashcards
Say the definition out loud before you flip the card. Retrieving it from memory is what builds recall, reading it again is not.
Reach the top grade on this topic
- Explain inheritance in terms of code reuse and a single place to fix shared behaviour
- Explain polymorphism with the argument about not needing to edit existing loops
- Use super() to extend a parent method rather than duplicating its code
- Choose inheritance for is a relationships and composition for has a
- Be able to name and explain all four pillars with a concrete example of each
Mistakes that cost marks here
- Forgetting to call super().__init__() so the parent's attributes are never set up.
- Using inheritance for a has a relationship, producing hierarchies that make no sense.
- Copying the parent's code into the subclass instead of calling super().
- Writing isinstance checks everywhere, which is exactly what polymorphism is meant to remove.
- Creating a deep chain of subclasses when one or two levels would do.
Polymorphism means many forms. It is what allows a game to loop through a list containing players, enemies, bullets and explosions and simply call update() on each, without ever asking what any of them actually is.
Knowledge check
super().__init__(name, age) do?Exam-style questions
Five written questions in the style of the real paper. Write a full answer, then let the marker check it against the mark scheme.
- LA new class is based on an existing class
- LIt takes on the attributes and methods of that class
- LIt can add new members or override existing ones
Inheritance is a relationship in which a new class, the subclass, is based on an existing class, the superclass. The subclass automatically takes on all the attributes and methods defined in the superclass, so shared behaviour only has to be written once. The subclass can then add attributes and methods of its own, and it can override any inherited method by defining its own version, which is then used in place of the parent's when called on a subclass object.
- LObjects of different classes respond to the same method call
- LEach provides its own implementation of that method
- LGives a valid example such as a list of shapes or animals
- LExplains that existing code does not need changing when a new class is added
Polymorphism means that objects of different classes can be used through the same interface, with each one responding to a method call in its own way. For example, a list might contain Circle, Square and Triangle objects, all inheriting from a Shape class, and a loop can simply call area() on each without ever asking what type it is holding, because each class provides its own version of the method. This is useful because it removes the need for long chains of type checks. Without polymorphism, every loop that processed shapes would need an if statement for each type, and adding a new shape would mean finding and editing every one of those loops. With polymorphism, a new shape is added by writing one new class, and all the existing code continues to work unchanged.
- LDefines the subclass inheriting from Animal
- LDefines a speak method with the same name
- LReturns or prints a dog specific sound
- LExplains that the subclass version replaces the parent version for objects of that class
class Dog(Animal): def speak(self): return "Woof" Overriding means defining a method in a subclass that has the same name as one in its parent class. When that method is called on an object of the subclass, Python uses the subclass version rather than the inherited one, so a Dog object returns Woof while another Animal subclass returns something different. The parent's version is not removed and can still be reached using super().speak() if the subclass wants to extend the original behaviour rather than replace it entirely.
- LInheritance expresses an is a relationship
- LComposition expresses a has a relationship, where an object contains another
- LUse inheritance when the subclass genuinely is a kind of the parent
- LUse composition when one object simply uses or contains another
Inheritance expresses an is a relationship: a Dog is a kind of Animal, so making Dog a subclass of Animal makes sense and Dog inherits everything an Animal can do. Composition expresses a has a relationship, where one object holds another as an attribute: a Car has an Engine, so the Car class creates an Engine object and stores it. The test for which to use is whether the sentence with is a makes sense. A Car is not a kind of Engine, so inheriting from Engine would produce a class hierarchy that is confusing and would give Car methods that make no sense for it. Choosing the wrong one is a common source of class hierarchies that nobody can follow, so the general advice is to prefer composition unless the is a relationship is genuinely true.
- LAbstraction is exposing only what is needed
- LAbstraction hides underlying complexity from the user of a class
- LEncapsulation is bundling data with the methods that act on it
- LEncapsulation controls access so data stays valid
- LInheritance is a class taking on the attributes and methods of another
- LInheritance avoids duplication because shared code is written once
- LPolymorphism is different classes responding to the same call in their own way
- LPolymorphism means new classes can be added without changing existing code
Abstraction means exposing only what is needed to use a class while hiding the complexity behind it. Someone using a list does not need to know how it stores items in memory, only that append adds one, and a well designed class works the same way. Encapsulation means bundling an object's data together with the methods that operate on it, and controlling how that data can be accessed from outside. Because changes go through the class's own methods, it can validate them and guarantee that the object never enters an invalid state, such as a bank account with a negative balance. Inheritance means a class can be built from an existing one, automatically taking on its attributes and methods and then adding or overriding whatever it needs. This avoids duplication, because behaviour shared by many classes is written once in the parent, and a correction made there applies everywhere at once. Polymorphism means that objects of different classes can respond to the same method call, each in its own way. A loop can call update on every object in a list without knowing or caring what type each one is, and because of this a new type can be added simply by writing a new class, with no change at all to the code that already uses them. Together these four principles produce code that is easier to reuse, easier to extend and far easier to keep correct as it grows.