Learning Goals
3 minBy the end of this lesson you can:
- Write a child class with
class Child(Parent):. - Inherit all the parent's attributes and methods automatically.
- Add child-specific attributes and methods.
- Use
isinstance(obj, Parent)— a child instance counts as both Child and Parent.
Warm-Up · The Repetition Problem
5 minYesterday's Hero and Monster both had:
def __init__(self, name, hp, attack_power): self.name = name self.hp = hp self.max_hp = hp self.attack_power = attack_power def is_alive(self): ... def take_damage(self, n): ... def attack(self, victim): ...
Hero adds inventory, xp, level, gain_xp, level_up. Monster doesn't. But every fight method is identical.
Copy-paste isn't a fix. If we patch a bug in Hero's take_damage, we have to remember to patch Monster too. Inheritance puts the shared code in one place.
Inheritance lets a Child class say "I'm a kind of Parent — give me everything the Parent has, and I'll add a few extras."
New Concept · class Child(Parent)
14 minThe shape
class Animal: def __init__(self, name): self.name = name def speak(self): print(f"{self.name} makes a generic noise.") class Dog(Animal): # Dog inherits from Animal pass fido = Dog("Fido") # Dog has Animal's __init__ fido.speak() # → Fido makes a generic noise. print(isinstance(fido, Dog)) # True print(isinstance(fido, Animal)) # True ← inheritance pays off
Dog wrote no code. It gets the constructor and the method for free.
Adding child-specific attributes
class Dog(Animal): def __init__(self, name, breed): super().__init__(name) # call the parent's __init__ self.breed = breed
The child's __init__ takes whatever extra arguments it needs, then calls super().__init__(name) to let the parent handle the shared setup. Don't reimplement the parent — delegate to it.
We'll dive into super() properly in PY-L3-08. For today, treat it as "call the parent's version of this method".
Adding child-specific methods
class Dog(Animal): def __init__(self, name, breed): super().__init__(name) self.breed = breed def fetch(self): # Dog-only method print(f"{self.name} fetches the ball.") fido = Dog("Fido", "Labrador") fido.speak() # inherited from Animal fido.fetch() # defined on Dog print(fido.breed) # → Labrador # Animal instances don't have .fetch() generic = Animal("???") # generic.fetch() # AttributeError
Multiple children
Many classes can inherit from the same parent.
class Cat(Animal): def purr(self): print(f"{self.name} purrs.") class Bird(Animal): def fly(self): print(f"{self.name} flies.") zoo = [Dog("Fido", "Lab"), Cat("Whiskers"), Bird("Tweety")] for a in zoo: a.speak() # every Animal has this — polymorphism preview
Every member of zoo is an Animal as far as .speak() is concerned. We'll formalise this idea in PY-L3-11.
The terminology
Animal parent / base / superclass Dog child / derived / subclass inherit get the parent's attrs and methods override redefine a method (next lesson) super() reference the parent's version
Why bother?
Three reasons:
- Less code. Write shared behaviour once.
- One source of truth. Fix a bug in one place; all children benefit.
- Polymorphism. Code that takes an
Animalworks with anyDog,CatorBirdwithout knowing the specific type.
Worked Example · Creature → Hero, Creature → Monster
12 minRefactor yesterday's code. Move the shared bits into Creature. Save as creatures.py:
# creatures.py — shared Creature parent import random class Creature: """Anything with HP that can fight.""" def __init__(self, name, hp, attack_power): self.name = name self.hp = hp self.max_hp = hp self.attack_power = attack_power def is_alive(self): return self.hp > 0 def take_damage(self, n): self.hp = max(0, self.hp - n) def heal(self, n): self.hp = min(self.max_hp, self.hp + n) def attack(self, victim): dmg = self.attack_power + random.randint(-2, 2) victim.take_damage(dmg) print(f" {self.name} hits {victim.name} for {dmg}. " f"({victim.name} HP {victim.hp}/{victim.max_hp})") class Hero(Creature): """A Creature with inventory, XP and levelling.""" def __init__(self, name, hp=100, attack_power=12): super().__init__(name, hp, attack_power) # let Creature do the basics self.inventory = [] self.xp = 0 self.level = 1 def pick_up(self, item): self.inventory.append(item) print(f" {self.name} picked up {item}.") def gain_xp(self, n): self.xp += n while self.xp >= self.level * 100: self.xp -= self.level * 100 self._level_up() def _level_up(self): self.level += 1 self.max_hp += 20 self.attack_power += 2 self.hp = self.max_hp print(f" ✨ {self.name} reaches Lv {self.level}!") class Monster(Creature): """A Creature that drops loot.""" def __init__(self, name, hp, attack_power, loot=None): super().__init__(name, hp, attack_power) self.loot = loot # Play aisyah = Hero("Aisyah", hp=80, attack_power=14) goblin = Monster("Goblin", hp=40, attack_power=8, loot="gold coin") # Both have take_damage, attack, is_alive — inherited from Creature while aisyah.is_alive() and goblin.is_alive(): aisyah.attack(goblin) if goblin.is_alive(): goblin.attack(aisyah) if aisyah.is_alive(): print(f"\n🏆 {aisyah.name} wins!") if goblin.loot: aisyah.pick_up(goblin.loot) aisyah.gain_xp(80) print(f"Inventory: {aisyah.inventory}") print(f"Level : {aisyah.level}, XP {aisyah.xp}") # Inheritance proof print(f"\nisinstance(aisyah, Hero) → {isinstance(aisyah, Hero)}") print(f"isinstance(aisyah, Creature) → {isinstance(aisyah, Creature)}") print(f"isinstance(goblin, Monster) → {isinstance(goblin, Monster)}") print(f"isinstance(goblin, Creature) → {isinstance(goblin, Creature)}") print(f"isinstance(goblin, Hero) → {isinstance(goblin, Hero)}")
Output (numbers will vary)
Aisyah hits Goblin for 13. (Goblin HP 27/40) Goblin hits Aisyah for 8. (Aisyah HP 72/80) Aisyah hits Goblin for 15. (Goblin HP 12/40) Goblin hits Aisyah for 9. (Aisyah HP 63/80) Aisyah hits Goblin for 14. (Goblin HP 0/40) 🏆 Aisyah wins! Aisyah picked up gold coin. Inventory: ['gold coin'] Level : 1, XP 80 isinstance(aisyah, Hero) → True isinstance(aisyah, Creature) → True isinstance(goblin, Monster) → True isinstance(goblin, Creature) → True isinstance(goblin, Hero) → False
Read the diff
The combat methods that yesterday existed in two places now exist in one. Hero and Monster each only contain what makes them unique. aisyah.attack(goblin) works because Hero inherits attack from Creature. The isinstance checks at the bottom show the family tree: a Hero is both a Hero and a Creature; a Monster is both a Monster and a Creature; but a Monster is not a Hero (different branches of the tree).
Try It Yourself
13 minDefine Vehicle with __init__(self, wheels) and method describe(). Define Car(Vehicle) with no body — just inherit. Build a Car and call describe().
Hint
class Vehicle: def __init__(self, wheels): self.wheels = wheels def describe(self): print(f"A vehicle with {self.wheels} wheels.") class Car(Vehicle): pass c = Car(4) c.describe() # → A vehicle with 4 wheels.
Zero new code in Car. It still works because Car inherits everything.
Extend Car: it has a brand as well. Override __init__ to take brand too; call super().__init__(4) inside.
Hint
class Car(Vehicle): def __init__(self, brand): super().__init__(4) # all cars have 4 wheels self.brand = brand c = Car("Perodua") c.describe() # → A vehicle with 4 wheels. print(c.brand) # → Perodua
The child stores its own attribute (brand) and delegates the rest to the parent.
Add Bike(Vehicle) (2 wheels), Truck(Vehicle) (6 wheels). Build one of each. Loop a list of all three and call describe().
Hint
class Bike(Vehicle): def __init__(self): super().__init__(2) class Truck(Vehicle): def __init__(self): super().__init__(6) garage = [Car("Perodua"), Bike(), Truck()] for v in garage: v.describe()
Three different classes; same loop calls the same method on each. That's polymorphism in action — the loop doesn't care which kind of vehicle.
Mini-Challenge · Shape Hierarchy
8 minBuild shapes.py. Define a Shape parent with method describe() that prints "A shape.". Then:
Rectangle(Shape)— takes width and height.area()returnsw * h.Square(Shape)— takes side.area()returnss * s.Triangle(Shape)— takes base and height.area()returns0.5 * b * h.
Loop a mixed list and print the area of each.
Show one possible solution
# shapes.py — Shape hierarchy class Shape: def describe(self): print("A shape.") class Rectangle(Shape): def __init__(self, w, h): self.w = w; self.h = h def area(self): return self.w * self.h class Square(Shape): def __init__(self, s): self.s = s def area(self): return self.s * self.s class Triangle(Shape): def __init__(self, b, h): self.b = b; self.h = h def area(self): return 0.5 * self.b * self.h shapes = [Rectangle(4, 6), Square(5), Triangle(8, 3)] for sh in shapes: name = type(sh).__name__ print(f" {name:<10} area = {sh.area()}")
Non-negotiables: each child inherits from Shape, each implements its own area, the loop calls .area() without caring which shape it is. The shared parent is what lets the loop be generic.
Recap
3 minclass Child(Parent): gives the child every attribute and method of the parent for free. Add child-specific things in the child's body. Override __init__ if you need extra parameters — and call super().__init__(...) inside to delegate the shared setup. A child instance counts as both Child and Parent in isinstance checks. The whole point is code reuse — one place for shared logic, child classes only describe what's different.
Vocabulary Card
- inherit
- Get the parent class's attributes and methods automatically.
- parent / child
- Also called base/derived or superclass/subclass. Same thing.
- super().__init__(...)
- Call the parent's constructor. Always do this when overriding
__init__. - is-a
- The relationship inheritance models. "A Hero is a Creature".
Homework
4 minBuild media.py. A parent class Media with attributes title, year and a describe() method. Then three children:
Book(Media)— addsauthorandpages.Movie(Media)— addsdirectorandruntime_min.Song(Media)— addsartistandduration_sec.
Each child should override describe() to print all its details. Build at least two of each child class and loop them all in one list calling describe.
Sample · media.py
class Media: def __init__(self, title, year): self.title = title; self.year = year def describe(self): print(f" {self.title} ({self.year})") class Book(Media): def __init__(self, title, year, author, pages): super().__init__(title, year) self.author = author; self.pages = pages def describe(self): print(f" 📖 {self.title} ({self.year}) by {self.author}, {self.pages}p") class Movie(Media): def __init__(self, title, year, director, runtime): super().__init__(title, year) self.director = director; self.runtime = runtime def describe(self): print(f" 🎬 {self.title} ({self.year}) dir. {self.director}, {self.runtime}m") class Song(Media): def __init__(self, title, year, artist, duration): super().__init__(title, year) self.artist = artist; self.duration = duration def describe(self): print(f" 🎵 {self.title} ({self.year}) by {self.artist}, {self.duration}s") library = [ Book("Charlotte's Web", 1952, "E.B. White", 192), Book("Wings of Fire", 2012, "Sutherland", 304), Movie("Inception", 2010, "Nolan", 148), Movie("Spirited Away", 2001, "Miyazaki", 125), Song("Levitating", 2020, "Dua Lipa", 203), Song("Cupid", 2023, "FIFTY FIFTY", 174), ] for m in library: m.describe()
Non-negotiables: a Media parent, three children with extra attributes, each child overrides describe(), and a polymorphic loop. Notice each child's __init__ takes the parent's args and passes them up with super.