You are viewing a free preview of this lesson.
Subscribe to unlock all 10 lessons in this course and every other course on LearningBro.
This lesson develops two of the four pillars of OOP: inheritance and polymorphism. Together they let you build hierarchies of related classes, eliminate duplicated code, and write flexible programs that operate on objects of many different types through a single common interface. They build directly on the class, object and encapsulation vocabulary from the previous lesson.
This lesson addresses inheritance and polymorphism within the object-oriented programming paradigm of the AQA A-Level Computer Science (7517) specification (subject content area 4.1.2). It covers: inheritance and the superclass/subclass relationship; the is-a relationship; overriding inherited methods; the use of super to call superclass behaviour; runtime (dynamic) polymorphism and how the method that executes is determined by the object's actual type; single versus multiple inheritance and the diamond problem; and the design decision between inheritance (is-a) and composition (has-a). You are expected to be able to design class hierarchies, trace polymorphic code and explain the benefits in terms of code reuse and extensibility.
Inheritance is the mechanism by which one class — the subclass (or child/derived class) — acquires the attributes and methods of another class — the superclass (or parent/base class). The subclass automatically gains everything the superclass exposes, and may then add new members or override inherited ones. Inheritance models the "is-a" relationship: a Dog is an Animal, a Manager is an Employee, a SavingsAccount is a BankAccount.
The pay-off is the Don't Repeat Yourself (DRY) principle. Attributes and behaviour common to a whole family of classes are written once in the superclass; each subclass inherits them rather than copying them. A bug fixed in the superclass is fixed for every subclass at once.
| Term | Definition |
|---|---|
| Superclass / parent / base class | The class being inherited from. |
| Subclass / child / derived class | The class that inherits from the superclass. |
| "Is-a" relationship | The relationship inheritance models: a Dog is an Animal. |
| Override | Replace an inherited method with a subclass-specific version. |
| Extend | Add new attributes or methods not present in the superclass. |
# AQA-style OOP pseudocode
CLASS Animal
PRIVATE name: STRING
PRIVATE sound: STRING
PUBLIC PROCEDURE new(n: STRING, s: STRING)
name = n
sound = s
END PROCEDURE
PUBLIC FUNCTION getName() RETURNS STRING
RETURN name
END FUNCTION
PUBLIC PROCEDURE speak()
OUTPUT name + " says " + sound
END PROCEDURE
END CLASS
CLASS Dog INHERITS Animal
PRIVATE breed: STRING
PUBLIC PROCEDURE new(n: STRING, b: STRING)
SUPER.new(n, "Woof") // initialise the inherited part first
breed = b
END PROCEDURE
PUBLIC FUNCTION getBreed() RETURNS STRING
RETURN breed
END FUNCTION
PUBLIC PROCEDURE fetch()
OUTPUT getName() + " fetches the ball!"
END PROCEDURE
END CLASS
class Animal:
def __init__(self, name: str, sound: str):
self._name = name # protected: subclasses may use it
self._sound = sound
def get_name(self) -> str:
return self._name
def speak(self) -> None:
print(f"{self._name} says {self._sound}")
class Dog(Animal):
def __init__(self, name: str, breed: str):
super().__init__(name, "Woof") # initialise inherited attributes
self.__breed = breed
def get_breed(self) -> str:
return self.__breed
def fetch(self) -> None:
print(f"{self.get_name()} fetches the ball!")
rex = Dog("Rex", "Labrador")
rex.speak() # Output: Rex says Woof (inherited from Animal)
rex.fetch() # Output: Rex fetches the ball! (added by Dog)
rex can call speak() even though Dog never defines it — the method is inherited from Animal. Note the use of a single underscore (_name) to mark attributes as protected so that subclasses can use them, in contrast to the double-underscore private attributes of the previous lesson.
Exam Tip: When explaining inheritance, anchor it to the "is-a" test. "A Dog is an Animal" is valid inheritance; "A Car has an Engine" is composition (a "has-a" relationship), not inheritance. Examiners reward this discriminator.
Method overriding occurs when a subclass provides its own implementation of a method already defined in its superclass. For objects of the subclass, the subclass version replaces the superclass version. This is the engine of runtime polymorphism.
class Shape:
def area(self) -> float:
return 0.0 # default, overridden below
def describe(self) -> None:
print(f"This shape has an area of {self.area():.2f}")
class Circle(Shape):
def __init__(self, radius: float):
self.__radius = radius
def area(self) -> float: # overrides Shape.area
return 3.14159 * self.__radius ** 2
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.__width = width
self.__height = height
def area(self) -> float: # overrides Shape.area
return self.__width * self.__height
Circle(5).describe() # This shape has an area of 78.54
Rectangle(4, 6).describe() # This shape has an area of 24.00
describe() is defined only in Shape, yet it calls self.area() and prints the correct area for each subclass. This is the crux of polymorphism: self.area() resolves to whichever area belongs to the object's actual class.
Polymorphism means "many forms". In OOP it allows objects of different classes to respond to the same method call in ways appropriate to each class. Two forms are distinguished:
| Type | Mechanism | Resolved |
|---|---|---|
| Runtime (dynamic) polymorphism | Method overriding — the version that runs is chosen from the object's actual type. | At run time |
| Compile-time (static) polymorphism | Method overloading — several methods share a name but differ in parameters (not supported natively in Python). | At compile time |
A-Level questions almost always concern runtime polymorphism through overriding.
def print_areas(shapes: list) -> None:
for shape in shapes:
print(f"Area: {shape.area():.2f}")
shapes = [Circle(3), Rectangle(4, 5), Circle(7)]
print_areas(shapes)
# Area: 28.27
# Area: 20.00
# Area: 153.94
print_areas works with any object that exposes an area() method. It never inspects the concrete type. Adding a new Triangle(Shape) class would work immediately, with no change to print_areas — this is the open/closed principle (open for extension, closed for modification).
Consider the list [Circle(3), Rectangle(4, 5)]. The table below traces how each shape.area() call is dispatched to the correct overridden method based on the object's actual class, even though the loop variable shape is declared with no specific type.
| Iteration | Object's actual class | area() resolves to | Computation | Result |
|---|---|---|---|---|
| 1 | Circle | Circle.area | 3.14159 × 3² | 28.27 |
| 2 | Rectangle | Rectangle.area | 4 × 5 | 20.00 |
The key insight examiners test: the method chosen depends on the run-time type of the object, not on the declared type of the variable that references it.
if isinstance(...) / if-elif type-checking chains.Exam Tip: When asked to trace polymorphic code, identify the actual object type at each call and look up the most-derived override of the method. Do not assume the superclass version runs just because the variable is "of the superclass type".
super keywordsuper() (or SUPER in AQA pseudocode) calls a method belonging to the superclass. Its most common use is inside a subclass constructor, to run the superclass constructor so that inherited attributes are properly initialised before the subclass adds its own.
class Employee:
def __init__(self, name: str, salary: float):
self._name = name
self._salary = salary
def pay_slip(self) -> str:
return f"{self._name}: £{self._salary:.2f}"
class Manager(Employee):
def __init__(self, name: str, salary: float, bonus: float):
super().__init__(name, salary) # initialise inherited attributes
self.__bonus = bonus
def pay_slip(self) -> str: # extend, don't duplicate
base = super().pay_slip() # reuse the superclass version
return f"{base} (+ £{self.__bonus:.2f} bonus)"
Here Manager.pay_slip extends rather than fully replaces the inherited behaviour: it calls super().pay_slip() to get the base slip, then appends the bonus. This is a powerful and exam-relevant pattern.
Classes form multi-level hierarchies; a subclass may itself be a superclass.
classDiagram
Vehicle <|-- Car
Vehicle <|-- Truck
Car <|-- Saloon
Car <|-- SUV
In this hierarchy Saloon inherits from Car, which inherits from Vehicle. A Saloon object therefore has access to members defined in Saloon, Car and Vehicle. When a method is called, the run-time system searches up the chain from the most-derived class and uses the first matching definition it finds — this lookup order is what makes overriding work.
| Type | Description | Support |
|---|---|---|
| Single inheritance | A class inherits from exactly one superclass. | All OOP languages. |
| Multiple inheritance | A class inherits from more than one superclass. | Python and C++ yes; Java and C# no (they use interfaces instead). |
Exam Tip: Multiple inheritance can cause the diamond problem: if classes B and C both inherit from A and override the same method, and D inherits from both B and C, it is ambiguous which version D should use. Python resolves this deterministically with the Method Resolution Order (MRO); Java sidesteps it by allowing only single class inheritance plus multiple interfaces.
Inheritance is powerful but easy to over-use. Where the relationship is "has-a" rather than "is-a", the correct tool is composition — building an object out of references to other objects.
| Relationship | Mechanism | Example |
|---|---|---|
| "is-a" | Inheritance | A SUV is a Car |
| "has-a" | Composition | A Car has an Engine |
Modern design guidance often advises favouring composition over inheritance, because deep inheritance trees are brittle: a change high in the hierarchy can ripple unexpectedly into many subclasses. Composition couples classes more loosely and is easier to change. You will explore this further in the abstract-classes lesson.
To trace polymorphic code confidently you must understand exactly how the run-time system decides which version of a method to run. When obj.method() is called, the system searches for method starting at the object's actual class and walking up the inheritance chain, stopping at the first definition it finds. This search order is precisely why an override in a subclass "wins" over the superclass version.
Consider a three-level hierarchy and trace a single call.
class Vehicle:
def describe(self) -> str:
return "A vehicle"
def horn(self) -> str:
return "Generic beep"
class Car(Vehicle):
def describe(self) -> str: # overrides Vehicle.describe
return "A car"
class SportsCar(Car):
def horn(self) -> str: # overrides Vehicle.horn
return "VROOM!"
sc = SportsCar()
print(sc.describe()) # ?
print(sc.horn()) # ?
The table below traces the search for each call. The system looks in SportsCar first, then Car, then Vehicle.
| Call | Search order | First match found | Result |
|---|---|---|---|
sc.describe() | SportsCar → Car | Car.describe | "A car" |
sc.horn() | SportsCar | SportsCar.horn | "VROOM!" |
describe() is not defined in SportsCar, so the search continues to Car, where it finds the override "A car" and stops — it never reaches Vehicle.describe. horn() is found immediately in SportsCar. This upward search is the mechanical heart of both inheritance (finding inherited methods) and overriding (the most-derived version is found first).
These two similarly named ideas are routinely confused, and distinguishing them cleanly is worth marks.
| Feature | Overriding | Overloading |
|---|---|---|
| Where | Across a superclass and subclass | Within a single class |
| Signatures | Same name and parameters | Same name, different parameters |
| Resolved | At run time (dynamic dispatch) | At compile time (by argument types) |
| Purpose | Specialise inherited behaviour | Offer several ways to call one operation |
| Python support | Yes | Not natively (last definition wins); simulated with default/variable arguments |
# Overriding: subclass redefines an inherited method (same signature)
class Animal:
def speak(self) -> str:
return "..."
class Cat(Animal):
def speak(self) -> str: # OVERRIDE
return "Meow"
# Overloading (as in Java/C#): same name, different parameter lists
# int add(int a, int b)
# double add(double a, double b, double c)
# Python instead uses default/optional parameters to similar effect:
class Calculator:
def add(self, a, b, c=0): # one method, flexible arity
return a + b + c
Exam Tip: If two methods have the same parameters and are in a superclass/subclass pair, it is overriding. If they have different parameters in the same class, it is overloading. State which mechanism you mean — examiners penalise using the terms interchangeably.
Class diagrams use distinct arrow styles for the different relationships, and reading them correctly is an examined skill.
| Relationship | UML arrow | Meaning |
|---|---|---|
| Inheritance | solid line, hollow triangle (`< | --`) |
| Composition | solid line, filled diamond | "Whole" strongly owns the "part" |
| Aggregation | solid line, hollow diamond | "Whole" has a "part" that can exist independently |
| Association | plain solid line | Objects of the two classes are linked |
The diagram below combines inheritance and composition: every Manager is an Employee (inheritance), and an Employee has a ContractDetails (composition).
classDiagram
Employee <|-- Manager
Employee *-- ContractDetails
class Employee {
-name: String
+paySlip() String
}
class Manager {
-bonus: Real
+paySlip() String
}
class ContractDetails {
-startDate: Date
-salary: Real
}
Being able to translate between such a diagram and code in both directions — diagram to class definitions, and code to diagram — is a common exam requirement.
Subscribe to continue reading
Get full access to this lesson and all 10 lessons in this course.