Inheritance
Reuse behaviour by saying 'is a kind of'
In this lesson
Inheritance lets one class build on another instead of repeating its code. When several types share most of their behaviour but differ in a few details, a parent class can hold the shared parts and each child can add or change just what’s special. This lesson keeps it practical — no deep family trees.
Explain it like I’m 5
Inheritance is like saying a dog is a kind of animal: it gets all the animal basics for free, then adds its own dog-specific behaviour on top.
Parent and child classes, and overriding
You create a child class by naming its parent in parentheses: class Dog(Animal):. The child automatically gets the parent’s attributes and methods. If the child defines a method with the same name as the parent’s, that’s overriding — the child’s version is used for child objects.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "..."
class Dog(Animal):
def speak(self):
return "Woof"
class Cat(Animal):
def speak(self):
return "Meow"
for animal in [Dog("Rex"), Cat("Mia")]:
print(animal.name, "says", animal.speak())
Rex says Woof Mia says Meow
Extending the parent with super()
Often a child needs to add to the parent’s setup rather than replace it. super().__init__(...) runs the parent’s __init__ first, then the child adds its own extra attributes. This avoids copying the parent’s setup code into every child.
class Account:
def __init__(self, owner):
self.owner = owner
self.balance = 0
class SavingsAccount(Account):
def __init__(self, owner, rate):
super().__init__(owner) # run Account's setup first
self.rate = rate
s = SavingsAccount("Ada", 0.05)
print(s.owner, s.balance, s.rate)
Ada 0 0.05
Common mistake: Forgetting super().__init__() in the child
The child defines its own __init__ and never calls the parent’s, so the parent’s attributes are never set.
Call super().__init__(...) at the top of the child’s __init__ so the parent’s setup runs before the child adds its own attributes.
Common mistake: Reaching for inheritance when things are only loosely related
Inheritance feels powerful, so it gets used to share a single method between classes that aren’t really the same kind of thing.
Only inherit when the child genuinely is a kind of the parent. For shared bits of behaviour between unrelated types, a helper function or composition (holding another object as an attribute) is usually clearer.
Common mistake: Overriding a method and dropping needed parent behaviour
A child rewrites a method from scratch and forgets the parent did something important in it.
If you only want to add to the parent’s behaviour, call super().the_method() inside the override and then do the extra work.
What does super() help you do?
super() reaches the parent class so you can reuse its behaviour instead of copying it.
What does method overriding mean?
Overriding gives the child its own version of a method while keeping everything else it inherited.
Why might composition (holding another object) sometimes beat inheritance?
When two things merely use each other rather than being the same kind of thing, composition models that more honestly than inheritance.
Mini exercise (medium)
Create a Vehicle parent class that stores a name, then Car and Bike child classes that each override describe() — for example "Tesla is a car with 4 wheels" and "BMX is a bike with 2 wheels". Loop over a mixed list of a car and a bike and print each description.
Do it here. Finish the code below and press Run to try your answer right in the browser — no setup needed.
class Vehicle:
def __init__(self, name):
self.name = name
def describe(self):
return f"{self.name} is a vehicle"
class Car(Vehicle):
def describe(self):
# TODO: return e.g. "Tesla is a car with 4 wheels"
return ""
class Bike(Vehicle):
def describe(self):
# TODO: return e.g. "BMX is a bike with 2 wheels"
return ""
for v in [Car("Tesla"), Bike("BMX")]:
print(v.describe())
Define class Car(Vehicle): and class Bike(Vehicle):, each with its own describe(). The parent's __init__ already gives them self.name.
class Vehicle:
def __init__(self, name):
self.name = name
def describe(self):
return f"{self.name} is a vehicle"
class Car(Vehicle):
def describe(self):
return f"{self.name} is a car with 4 wheels"
class Bike(Vehicle):
def describe(self):
return f"{self.name} is a bike with 2 wheels"
for v in [Car("Tesla"), Bike("BMX")]:
print(v.describe())
Tesla is a car with 4 wheels
BMX is a bike with 2 wheels
__no_output_bypass__ = True
import ast as _ast
_bases = []
for _n in _ast.walk(_ast.parse(__user_code__)):
if isinstance(_n, _ast.ClassDef):
_bases += [b.id for b in _n.bases if isinstance(b, _ast.Name)]
assert _bases.count("Vehicle") >= 2, "Car and Bike should both inherit from Vehicle"
__no_output_bypass__ = False
assert Car("Tesla").describe() == "Tesla is a car with 4 wheels", "Car.describe() should mention a car with 4 wheels"
assert Bike("BMX").describe() == "BMX is a bike with 2 wheels", "Bike.describe() should mention a bike with 2 wheels"
assert isinstance(Car("x"), Vehicle), "a Car object should also be a Vehicle"
print("✓ One loop, different behaviour!")