Learning Goals
3 minBy the end of this lesson you can:
- Override a parent method by redefining it in the child with the same name.
- Call the parent's version from inside the override with
super().method(...). - Choose between "replace" and "extend" depending on whether you still want the parent's behaviour.
- Recognise when
super()is necessary — and when it isn't.
Warm-Up · You've Already Overridden
5 minIn yesterday's homework, each Media child class redefined describe() with a custom format. That's overriding.
class Media: def describe(self): print(f" {self.title} ({self.year})") class Book(Media): def describe(self): # ← override print(f" 📖 {self.title} by {self.author}")
When you call book.describe(), Python uses the child's version. The parent's version is hidden — Python checks the child first.
An override replaces the method. super() lets you keep and extend the parent's behaviour. Two flavours of customisation.
New Concept · Replace vs Extend
14 minPure override · the "replace" pattern
When you want completely different behaviour, just redefine the method:
class Animal: def speak(self): print("Generic noise.") class Dog(Animal): def speak(self): # ← totally replaces print("Woof!") class Cat(Animal): def speak(self): print("Meow.") Dog().speak() # → Woof! Cat().speak() # → Meow. Animal().speak() # → Generic noise.
Same method name, different body. No call to super() — we don't want any of the parent's behaviour.
super() · the "extend" pattern
When you want the parent's behaviour plus a bit extra, call super().method(...) inside your override:
class Logger: def save(self, data): with open("log.txt", "a") as f: f.write(data + "\n") print("Saved.") class TimestampLogger(Logger): def save(self, data): from datetime import datetime stamped = f"[{datetime.now().isoformat()}] {data}" super().save(stamped) # ← parent does the heavy lifting
The child adds the timestamp; the parent still handles the file writing. Each class does its bit.
The classic example · __init__
Almost every child __init__ calls super().__init__(...):
class Vehicle: def __init__(self, wheels, max_speed): self.wheels = wheels self.max_speed = max_speed class Car(Vehicle): def __init__(self, brand, max_speed): super().__init__(wheels=4, max_speed=max_speed) self.brand = brand # extra Car-only attr
The parent handles wheels and max_speed. The child only writes the brand-new attribute. Clean.
The forgotten super() bug
If you forget super().__init__(), the parent's attributes are never set:
class Vehicle: def __init__(self, wheels): self.wheels = wheels class Bike(Vehicle): def __init__(self): # forgot super! self.brand = "Schwinn" b = Bike() print(b.wheels) # AttributeError: 'Bike' object has no attribute 'wheels'
The Bike has a brand but no wheels. Always call super().__init__ when overriding __init__ — unless you have a deliberate reason not to.
Calling another method via super
class Document: def save(self): print("Saving document...") class SignedDocument(Document): def save(self): print("Signing...") super().save() print("Done signing + saving.") SignedDocument().save() # Signing... # Saving document... # Done signing + saving.
Three behaviours in one method — before, parent call, after. The classic "sandwich" pattern.
When you DON'T need super()
If the child's logic completely replaces the parent's — the "Dog says Woof, not generic noise" case — skip super entirely.
Worked Example · Animal Hierarchy
12 minSave as animals.py:
# animals.py — override + super patterns class Animal: def __init__(self, name, sound="generic noise"): self.name = name self.sound = sound print(f"[Animal] {self.name} was born.") def speak(self): print(f" {self.name} says: {self.sound}") def describe(self): print(f"=== {self.name} ===") self.speak() class Dog(Animal): """Pure override — totally different speak.""" def speak(self): print(f" {self.name} barks: WOOF WOOF!") class Cat(Animal): """Pure override — totally different speak.""" def speak(self): print(f" {self.name} purrs: meow~") class TimestampedAnimal(Animal): """Extend pattern — wrap parent's speak with timestamps.""" def speak(self): from datetime import datetime print(f" [{datetime.now().strftime('%H:%M:%S')}]") super().speak() # delegate to parent print(f" (end of message)") class Parrot(Animal): """Extend __init__ — add an extra attribute.""" def __init__(self, name, vocabulary): super().__init__(name, sound="squawk") self.vocabulary = vocabulary # Parrot-only def speak(self): super().speak() # parent's speak first import random word = random.choice(self.vocabulary) print(f" Then says: {word!r}") # Test print("\n--- Dog ---") fido = Dog("Fido") fido.describe() print("\n--- Cat ---") whiskers = Cat("Whiskers") whiskers.describe() print("\n--- Timestamped ---") ts = TimestampedAnimal("Bear") ts.describe() print("\n--- Parrot ---") polly = Parrot("Polly", vocabulary=["hello", "cracker", "pretty bird"]) polly.describe() polly.describe() # different word each time
Sample output
[Animal] Fido was born. --- Dog --- === Fido === Fido barks: WOOF WOOF! [Animal] Whiskers was born. --- Cat --- === Whiskers === Whiskers purrs: meow~ [Animal] Bear was born. --- Timestamped --- === Bear === [22:31:04] Bear says: generic noise (end of message) [Animal] Polly was born. --- Parrot --- === Polly === Polly says: squawk Then says: 'cracker'
Read the diff
Four children, three patterns. Dog and Cat replace speak entirely. TimestampedAnimal extends speak — same parent behaviour plus timestamps. Parrot extends both __init__ (super() + extra attribute) and speak (super() + extra print). Notice describe is inherited from Animal — it doesn't care which kind of animal it's describing; it just calls self.speak() and the right version runs. That's polymorphism, formalised in lesson 11.
Try It Yourself
13 minDefine Greeter with greet(self): print("Hi"). Define ShoutyGreeter(Greeter) that overrides to print "HI!!!". Test both.
Hint
class Greeter: def greet(self): print("Hi.") class ShoutyGreeter(Greeter): def greet(self): print("HI!!!") Greeter().greet() # → Hi. ShoutyGreeter().greet() # → HI!!!
Define VerboseGreeter(Greeter) that prints a prefix line, calls super().greet(), then prints a suffix line.
Hint
class VerboseGreeter(Greeter): def greet(self): print("--- start ---") super().greet() print("--- end ---") VerboseGreeter().greet() # --- start --- # Hi. # --- end ---
The classic sandwich: before, super(), after.
Define Pet with __init__(name). Then WorkingDog(Pet) with extra attribute job — call super().__init__(name), then set self.job. Build one and confirm both attrs work.
Hint
class Pet: def __init__(self, name): self.name = name class WorkingDog(Pet): def __init__(self, name, job): super().__init__(name) self.job = job w = WorkingDog("Rex", "police") print(w.name, w.job) # → Rex police
Mini-Challenge · Discounted Cart
8 minBuild discount_cart.py. Take the Cart class from PY-L3-04 homework as the parent. Create a child StudentCart(Cart) that:
- Overrides
total()to apply a 15% student discount on top of any existing discount. - Uses
super().total()to get the base total first. - Add a method
show_savings()that prints the difference between base total and discounted total.
Show one possible solution
# discount_cart.py — child class with override + super class Cart: def __init__(self, holder="guest"): self.holder = holder self.items = [] def add(self, name, price): self.items.append((name, price)) def total(self): return sum(p for _, p in self.items) class StudentCart(Cart): STUDENT_DISCOUNT = 0.15 def total(self): base = super().total() # parent computes raw total return base * (1 - self.STUDENT_DISCOUNT) def show_savings(self): base = super().total() saved = base - self.total() print(f" Saved RM {saved:.2f} with student discount") c = StudentCart("Aisyah") c.add("nasi lemak", 8.0) c.add("teh tarik", 3.5) print(f"Total : RM {c.total():.2f}") # 15% off 11.50 → 9.78 c.show_savings()
Non-negotiables: child overrides total(), uses super().total() inside, and adds a show_savings method that also uses super. STUDENT_DISCOUNT is a class-level constant — same for every StudentCart.
Recap
3 minOverriding a method = redefining it in the child. Python uses the child's version when called on a child instance. To extend the parent rather than replace it, call super().method(...) inside the override. The classic case is __init__ — almost every child __init__ starts with super().__init__(...). Forgetting that leaves the parent's attributes unset. Pure replace skips super; extend uses it.
Vocabulary Card
- override
- Define a method in the child with the same name as a parent's. Hides the parent's version.
- super()
- A reference to the parent class. Use
super().method(args)to call the parent's version. - replace pattern
- Override with no super call. The child does everything itself.
- extend pattern
- Override that calls super inside. The child adds behaviour around the parent's.
Homework
4 minTake the Media classes from PY-L3-07 homework. Add a like() method to the Media parent that increases self.likes (init to 0) and prints "Liked!". Then in Movie, override like() to also increment a self.oscar_buzz counter and print the running total. Use super().like() to still get the parent's behaviour.
Sample · key changes
class Media: def __init__(self, title, year): self.title = title self.year = year self.likes = 0 def like(self): self.likes += 1 print(f"Liked! {self.title} now has {self.likes} likes.") class Movie(Media): def __init__(self, title, year, director, runtime): super().__init__(title, year) self.director = director self.runtime = runtime self.oscar_buzz = 0 def like(self): super().like() # parent does the counting + print self.oscar_buzz += 1 print(f" Oscar buzz: {self.oscar_buzz}") m = Movie("Inception", 2010, "Nolan", 148) m.like() m.like() m.like() # Liked! Inception now has 1 likes. # Oscar buzz: 1 # Liked! Inception now has 2 likes. # Oscar buzz: 2 # Liked! Inception now has 3 likes. # Oscar buzz: 3
Non-negotiables: parent adds a like counter, child extends it with extra behaviour via super(). The Media class doesn't know about oscar_buzz at all — the child layered it on cleanly.