Grade by Grade
C. Designing Modular Programs · Modular and Object-Oriented Design

Overriding, Overloading and Polymorphism

A2.2 C1.4About 75 minutes
Learning goals
  • I can override an inherited method and extend it with super().method(...).
  • I can explain polymorphism and write a loop that treats objects of different subclasses the same way.
  • I can use an abstract base class to require that every subclass provides a method.
  • I can explain method overloading, why Python does not support it by signature, and how default parameters give the same effect.
  • I can overload operators such as +, == and < with special methods.

Words to know

overriding
Defining a method in a subclass with the same name (and parameters) as an inherited method, so the subclass's version is used.
polymorphism
"Many forms": the same method call works on objects of different classes, and each responds in its own way.
dynamic dispatch
Choosing which method body to run at run time, based on the object's actual class.
abstract class
A class that is not meant to have objects of its own; it defines methods that subclasses must provide. In Python, a subclass of ABC with @abstractmethod methods.
concrete class
A class that provides all of its methods and can be instantiated.
method overloading
Several methods with the same name but different parameter lists in one class (supported in Java and C++, not by signature in Python).
operator overloading
Giving an operator such as + or < a meaning for your own class by defining special methods like __add__ and __lt__.
duck typing
Python's style of polymorphism: any object that has the needed methods can be used, whatever its class.
special (dunder) method
A method with double underscores on both sides, such as __str__, that Python calls automatically for built-in operations.

Same question, different answers

Ask three rental items "What do you cost for 4 days?" Poles answer with the plain rate. A helmet adds a cleaning fee. A snowboard gives every 4th day free. The question is the same; the answer depends on what kind of item you asked.

This lesson covers the three remaining ideas named in expectation A2.2: method overriding (a subclass changes an inherited method), polymorphism (one call, many behaviours) and method overloading (one name, different parameter lists). Together with encapsulation and inheritance, they are the toolkit of object-oriented design.

Overriding

To override, define a method in the subclass with the same name and parameters as the inherited one. Because Python searches the object's own class first (the MRO from last lesson), the subclass version wins.

Often you want to extend the inherited behaviour rather than replace it. Call the superclass version with super().cost(days) and add to its result. This reuses the superclass's logic, so if the base rate rule changes in Item, Helmet's cost changes with it automatically.

You have already overridden methods without knowing it: every class inherits a plain __str__ from object, and writing your own __str__ overrides it.

Worked example

Example 1: Overriding cost

Write Helmet and Snowboard subclasses of Item. Helmets cost the normal rate plus a one-time 200-cent cleaning fee. Snowboards get every 4th day free. Helmets also show the fee in their description. Print each item and its cost for 4 days.

  1. Helmet.cost calls super().cost(days) for the base amount (800 × 4 = 3200) and adds 200: 3400.
  2. Helmet.__str__ extends Item's text: it takes super().__str__() and adds " (+$2.00 cleaning)".
  3. Snowboard.cost works out the free days with integer division: 4 // 4 = 1, then charges super().cost(4 - 1) = 4000 × 3 = 12 000.
  4. Plain Item P1 (ski poles) uses the original method: 1500 × 4 = 6000.
  5. The loop never asks what kind of item it has. It just calls g.cost(4). That is polymorphism.

Code and output below.

class Item:
"""Any piece of rental equipment."""
def __init__(self, item_id, daily_cents):
self.item_id = item_id
self.daily_cents = daily_cents
def cost(self, days):
return self.daily_cents * days
def __str__(self):
return f"{self.item_id}: ${self.daily_cents / 100:.2f}/day"
class Helmet(Item):
CLEANING_CENTS = 200
def cost(self, days): # overrides Item.cost
return super().cost(days) + Helmet.CLEANING_CENTS
def __str__(self): # overrides Item.__str__
return super().__str__() + " (+$2.00 cleaning)"
class Snowboard(Item):
def cost(self, days): # overrides Item.cost
"""Every 4th day is free."""
free_days = days // 4
return super().cost(days - free_days)
gear = [Item("P1", 1500), Helmet("H1", 800), Snowboard("B1", 4000)]
for g in gear:
print(g, "->", g.cost(4))

overriding.py: two subclasses override cost, and Helmet also overrides __str__.

P1: $15.00/day -> 6000
H1: $8.00/day (+$2.00 cleaning) -> 3400
B1: $40.00/day -> 12000

Output of the program above (what Python prints).

Polymorphism

Polymorphism means "many forms". Code written against a superclass, like for g in gear: g.cost(4), works for every subclass, including subclasses written next year by someone else. Python decides at run time which cost to run by looking at each object's actual class; this is called dynamic dispatch.

Polymorphism is what makes inheritance pay off. Without it, you would write if type(g) == Helmet: ... elif type(g) == Snowboard: ... everywhere, and adding a new item kind would mean editing every one of those if-chains. With it, you add one subclass and the existing loops just work. This is sometimes called the open-closed principle: code should be open for extension (new subclasses) but closed for modification (no edits to working code).

for s in shapes:s.area()Circle.areaπ × r²12.57Rectangle.areaw × h12Triangle.areabase × height ÷ 215.0Python picks the method from each object's own class at run time.
Polymorphism: the loop makes the same call on every object, and each object answers with its own version of area. The loop never checks what kind of shape it has.

Abstract classes: a contract for subclasses

Sometimes the superclass cannot provide a sensible method at all. What is the area of a "shape" in general? Nothing. A Shape is an abstract idea; only circles, rectangles and triangles are concrete.

Python's abc module (abstract base classes) lets you say so. Make the class inherit from ABC and mark the method with @abstractmethod. Then:

  • trying to create a Shape() raises TypeError;
  • any subclass that forgets to override area also cannot be instantiated, so the mistake shows up immediately, not months later.

The abstract class can still contain concrete methods, such as describe(), that call the abstract one. Every subclass inherits describe() and it automatically uses that subclass's own area.

Worked example

Example 2: A shape hierarchy

Write an abstract Shape class with an abstract area() and a concrete describe(). Write Circle, Rectangle and Triangle. Print a description of each shape in a list and the total area.

  1. class Shape(ABC): with @abstractmethod def area(self):. The docstring is the method's whole body, which is allowed.
  2. describe() is written once, in Shape, and calls self.area(). Which area runs depends on the object.
  3. Areas: circle π × 2² ≈ 12.566; rectangle 3 × 4 = 12; triangle 6 × 5 ÷ 2 = 15.0. The format :.1f shows one decimal place.
  4. Total: 12.566 + 12 + 15 = 39.566, rounded to 39.57.
  5. Shape() raises TypeError because Shape is abstract.

Code and output below. Adding a new shape (say, a Hexagon) needs one new class; the loop and describe() need no changes.

import math
from abc import ABC, abstractmethod
class Shape(ABC):
"""Abstract superclass: every shape can report its area."""
@abstractmethod
def area(self):
"""Return the area. Every concrete subclass MUST override this."""
def describe(self):
return f"{type(self).__name__} with area {self.area():.1f}"
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return math.pi * self.r ** 2
class Rectangle(Shape):
def __init__(self, w, h):
self.w = w
self.h = h
def area(self):
return self.w * self.h
class Triangle(Shape):
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return self.base * self.height / 2
shapes = [Circle(2), Rectangle(3, 4), Triangle(6, 5)]
total = 0
for s in shapes:
print(s.describe()) # one call, three different area methods
total += s.area()
print("Total:", round(total, 2))
try:
Shape()
except TypeError:
print("Shape is abstract: you cannot create one")

shapes.py: an abstract base class with three concrete subclasses.

Circle with area 12.6
Rectangle with area 12.0
Triangle with area 15.0
Total: 39.57
Shape is abstract: you cannot create one

Output of the program above (what Python prints).

Method overloading

In Java or C++, one class can have several methods with the same name but different parameter lists, and the compiler picks one by looking at the arguments:

  • area(double r) for a circle, and
  • area(double w, double h) for a rectangle.

This is overloading. Python does not work this way. A def just assigns a function to a name, so a second def area replaces the first, whatever its parameters.

def area(r):
return 3.14159 * r * r
def area(w, h): # this REPLACES the first area completely
return w * h
print(area(3, 4))
try:
area(2)
except TypeError:
print("area(2) fails: the one-parameter version is gone")

In Python, the second def wins. There is only ever one function called area.

12
area(2) fails: the one-parameter version is gone

Output of the program above (what Python prints).

Python programmers get the effect of overloading in other ways:

  • Default parameters, as in def area(a, b=None):, so one function accepts one or two arguments.
  • Keyword arguments, such as make_card(owner, balance=0).
  • Variable numbers of arguments, written with a star before a parameter name (for example, a function that adds any number of prices).
  • Checking the type of an argument with isinstance, or the standard-library tool functools.singledispatch, which picks a function by the type of the first argument.

The idea behind overloading, offering one name for one concept that can take different inputs, is still good design. Python just implements it inside a single function.

import math
def area(a, b=None):
"""Circle area for area(r); rectangle area for area(w, h)."""
if b is None:
return math.pi * a * a
return a * b
print(round(area(2), 2), area(3, 4))

One function, two ways to call it. Output: 12.57 12

Overridingsame name, same parameters,in a SUBCLASSItem.cost(days)Helmet.cost(days)overridesOverloadingsame name, DIFFERENT parameters,in the same class or moduleJava: area(r) and area(w, h)both exist side by sidePython: area(a, b=None)one function handles both
Overriding happens across a hierarchy (a subclass replaces an inherited method). Overloading means one name with different parameter lists. Languages like Java support it directly; Python keeps only the last definition, so it uses default parameters instead.

Operator overloading

Python does support overloading operators. You already did this for comparisons in Making Objects Comparable (Data Types, Files and Expressions): a == b calls a.__eq__(b) and a < b calls a.__lt__(b). The same mechanism works for arithmetic: a + b calls a.__add__(b) and a - b calls a.__sub__(b). Define those special methods in your class and the operators work on your objects. Built-in functions use them too: sorted() and min() compare with <, len(x) calls x.__len__(), and print(x) calls x.__str__().

Overload an operator only when its meaning is obvious. Adding two amounts of money is obvious. "Adding" two students is not; give that a named method instead.

Worked example

Example 3: A Money class with operators

Write a Money class that stores cents and supports + (new) along with == and < (as in Making Objects Comparable) and readable printing. Add lunch ($12.50) and a snack ($3.75), compare amounts, and sort a list of prices.

  1. __add__(self, other) returns a new Money object, so lunch + snack does not change either operand. 1250 + 375 = 1625 cents = $16.25.
  2. __eq__ compares cents. Without it, == would compare identity and Money(500) == Money(500) would be False.
  3. __lt__ compares cents. Defining it is enough for sorted() to order a list of Money objects.
  4. __str__ formats dollars; the list comprehension converts each to text for printing.

Code and output below.

class Money:
"""An amount of money in whole cents, with operators overloaded."""
def __init__(self, cents):
self.cents = cents
def __add__(self, other): # runs for a + b
return Money(self.cents + other.cents)
def __eq__(self, other): # runs for a == b
return self.cents == other.cents
def __lt__(self, other): # runs for a < b (and lets sorted() work)
return self.cents < other.cents
def __str__(self):
return f"${self.cents / 100:.2f}"
lunch = Money(1250)
snack = Money(375)
print(lunch + snack)
print(Money(500) == Money(500), snack < lunch)
prices = sorted([lunch, snack, Money(899)])
print([str(p) for p in prices])

money.py: three operators and __str__ overloaded.

$16.25
True True
['$3.75', '$8.99', '$12.50']

Output of the program above (what Python prints).

Just for fun

The word polymorphism comes from Greek words meaning "many shapes", which is why nearly every textbook teaches it with a Shape class. Computer scientists are not always creative, but they are consistent.

Hands-on activity

The noise-making zoo

You need: a computer with Python and a family member to add the "surprise" class.

  1. Write an abstract class Animal(ABC) with a name attribute, an abstract method sound(), and a concrete method speak() that returns f"{self.name} says {self.sound()}".
  2. Write three concrete subclasses for animals found in Canada, for example Loon, Moose and Beaver. Each overrides sound() (a loon's call is often described as a wail or yodel; any reasonable word is fine).
  3. Put one of each in a list and print a.speak() in a loop.
  4. Ask your family member to write a fourth subclass without looking at your loop. Add it to the list. The loop should work without any change. That is the open-closed principle in action.
  5. Try making a subclass that forgets sound(). Create one and read the error.
  6. Bonus: overload __lt__ so animals sort by name, and print sorted(zoo).

What to look for: no if type(...) checks in the loop; the forgotten-method subclass raises TypeError when instantiated; sorting works after __lt__ is added.

Your turn

1

What is the difference between overriding and overloading?

2

Using Example 1's classes, what is Helmet("H2", 1000).cost(2)? And Snowboard("B2", 4000).cost(9)?

3

What does super().cost(days) do inside Helmet.cost?

4

Define polymorphism and give an example from this lesson.

5

What happens when you run Shape() for the abstract Shape class? Why is that useful?

6

Predict the output:

def greet(name):

    return "Hi " + name

def greet(name, title):

    return "Hello " + title + " " + name

print(greet("Ali", "Dr."))

7

Which special method must Money define for sorted([...]) to work?

More practice: warm-up

1

What does the word polymorphism mean?

2

Which module provides ABC and abstractmethod?

3

Which special method runs for a + b?

4

Which special method runs for a == b?

5

True or false: Python keeps both versions when you write two functions with the same name and different parameters.

More practice: on your own

1

Add a Square class to the shape hierarchy, as a subclass of Shape with one side length. What does Square(5).describe() return?

2

Write a function total(prices, tax_rate=0.13) returning the sum plus tax, rounded to cents. What is total([10, 6])? And total([10, 6], 0)?

3

Add __sub__ to Money so Money(1000) - Money(250) prints $7.50.

4

Rewrite this code so it uses polymorphism instead of type checks:

for g in gear:

    if type(g) == Helmet:

        print(g.daily_cents * 4 + 200)

    else:

        print(g.daily_cents * 4)

5

A subclass Hexagon(Shape) defines def area(self, side): with an extra parameter. Why does Hexagon(...).describe() crash, and what does this teach about overriding?

Challenge

1

Predict the output and explain each line:

class A:

    def who(self):

        return "A"

    def hello(self):

        return "hello from " + self.who()

class B(A):

    def who(self):

        return "B"

print(A().hello(), "|", B().hello())

2

After adding __eq__ to Money, {Money(500): "five"} raises TypeError: unhashable type. Explain why, and fix it.

3

Use functools.singledispatch to write describe(x) that returns "number: ..." for an int and "text: ..." for a str, with a general fallback. Why is this closer to true overloading than default parameters?

Check your understanding

Quiz

1.

A subclass method has the same name and parameters as a superclass method. This is called:

2.

Using Example 1, what does Snowboard("B3", 3000).cost(8) return?

3.

What is the main benefit of polymorphism?

4.

In Python, what happens when you define two functions named area with different parameter lists in the same module?

5.

Which special method lets you use < on your own objects?

6.

What does @abstractmethod on Shape.area guarantee?

7.

Inside Helmet.cost, what does super().cost(days) return for days = 3 and a daily rate of 800?

For parents and teachers

Big idea: A2.2 lists method overloading, method overriding and polymorphism alongside encapsulation and inheritance as concepts that support reusable code. Overriding changes inherited behaviour; polymorphism lets one loop handle many kinds of object; overloading (one name, several parameter lists) works differently in Python than in Java, and students should know both views because university courses often use Java or C++.

How to help: The zoo activity is a good chance to be the "other programmer" who adds a new class. Celebrate when the loop works unchanged; that is the whole point.

Common difficulties:

  • Confusing overriding (subclass, same parameters) with overloading (different parameters).
  • Writing if type(x) == ... chains instead of overriding a method.
  • Changing the parameters of an overridden method, which breaks code written for the superclass.
  • Expecting Python to keep two functions with the same name.