AiTechWorlds
AiTechWorlds
Walk into an orchestra rehearsal. The conductor raises the baton and calls out: "Play!"
The violinist draws the bow across the strings — a long, resonant note fills the hall. The pianist presses the keys — bright, percussive tones ring out. The drummer strikes the snare — a sharp crack cuts through the room.
Each musician received the same instruction: play. Each responded in a completely different way, appropriate to their instrument. There was no need for the conductor to say "violinists, bow your strings; pianists, press your keys." The instruction was uniform; the execution was specialised.
This is polymorphism in action, and method overriding is the mechanism that makes it work.
Method overriding occurs when a child class defines a method with the same name as a method in the parent class. The child's version replaces the parent's version for objects of that child type.
class Instrument:
def play(self):
print("(generic instrument sound)")
class Violin(Instrument):
def play(self): # Overrides Instrument.play
print("Violin: bowing strings — ♩ ♪ ♫")
class Piano(Instrument):
def play(self): # Overrides Instrument.play
print("Piano: pressing keys — ding ding ding")
class Drum(Instrument):
def play(self): # Overrides Instrument.play
print("Drum: striking head — BOOM tss BOOM tss")
The power of method overriding becomes clear when you treat all objects through their common parent type:
orchestra = [Violin(), Piano(), Drum(), Violin()]
for instrument in orchestra:
instrument.play() # Same call — different behaviour for each type
Output:
Violin: bowing strings — ♩ ♪ ♫
Piano: pressing keys — ding ding ding
Drum: striking head — BOOM tss BOOM tss
Violin: bowing strings — ♩ ♪ ♫
The loop does not care what type each instrument is. It simply calls play() and each object responds correctly. Adding a new instrument type (Trumpet, Flute) requires zero changes to the loop.
import math
class Shape:
"""Base class for all shapes."""
def __init__(self, colour="black"):
self.colour = colour
def area(self):
"""To be overridden by every subclass."""
raise NotImplementedError(
f"{self.__class__.__name__} must implement area()"
)
def perimeter(self):
"""To be overridden by every subclass."""
raise NotImplementedError(
f"{self.__class__.__name__} must implement perimeter()"
)
def describe(self):
"""Shared method — uses overridden area() and perimeter()."""
return (
f"{self.__class__.__name__} | colour={self.colour} | "
f"area={self.area():.2f} | perimeter={self.perimeter():.2f}"
)
class Circle(Shape):
def __init__(self, radius, colour="black"):
super().__init__(colour)
self.radius = radius
def area(self): # Override
return math.pi * self.radius ** 2
def perimeter(self): # Override
return 2 * math.pi * self.radius
class Rectangle(Shape):
def __init__(self, width, height, colour="black"):
super().__init__(colour)
self.width = width
self.height = height
def area(self): # Override
return self.width * self.height
def perimeter(self): # Override
return 2 * (self.width + self.height)
class Triangle(Shape):
def __init__(self, a, b, c, colour="black"):
super().__init__(colour)
self.a = a # Three side lengths
self.b = b
self.c = c
def area(self): # Override — Heron's formula
s = (self.a + self.b + self.c) / 2 # Semi-perimeter
return math.sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))
def perimeter(self): # Override
return self.a + self.b + self.c
shapes = [
Circle(5, colour="red"),
Rectangle(4, 6, colour="blue"),
Triangle(3, 4, 5, colour="green"),
Circle(1),
]
for shape in shapes:
print(shape.describe())
Output:
Circle | colour=red | area=78.54 | perimeter=31.42
Rectangle | colour=blue | area=24.00 | perimeter=20.00
Triangle | colour=green | area=6.00 | perimeter=12.00
Circle | colour=black | area=3.14 | perimeter=6.28
Notice that describe() is defined only in Shape — once. But when it calls self.area() and self.perimeter(), Python dispatches to the actual object's class at runtime. Shape.describe() automatically benefits from every subclass's overrides.
super()Sometimes you want to add to the parent's behaviour, not replace it entirely. Use super().method() to run the parent first (or last):
class Shape:
def describe(self):
return f"Shape with colour={self.colour}"
class Circle(Shape):
def describe(self):
parent_description = super().describe() # Get parent output first
return f"{parent_description}, radius={self.radius}" # Extend it
c = Circle(7, colour="purple")
print(c.describe())
Output:
Shape with colour=purple, radius=7
@override Decorator (Python 3.12+)Python 3.12 introduced @override from typing to make overrides explicit and verifiable:
from typing import override
class Shape:
def area(self) -> float:
raise NotImplementedError
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
@override
def area(self) -> float: # Type checkers verify this overrides a real parent method
return math.pi * self.radius ** 2
If you accidentally misspell the method name (def erea(self):), a type checker like mypy or pyright will immediately flag the error — @override declared but no matching method found in parent.
| Scenario | Action |
|---|---|
| Child behaviour is completely different from parent | Full override — don't call super() |
| Child extends parent behaviour | Call super().method() then add to it |
| Enforcing that subclasses MUST implement a method | Use raise NotImplementedError in the parent |
| Python 3.12+ codebase with type hints | Add @override for safety |
shape.describe() doesn't care whether the shape is a Circle or a Triangle.raise NotImplementedError in a base class to force subclasses to provide their own implementation.super().method() when you want to extend parent behaviour rather than replace it entirely.@override decorator (Python 3.12+) makes your intentions explicit and allows type checkers to catch typos.Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises