What you'll learn
- Create a subclass that inherits everything from a parent class
- Override a method and extend it with
super()instead of replacing it - Read the method resolution order to predict which method runs
- Know the one question that tells you whether inheritance is the right tool
Concept
Inheritance lets one class be built on another. class SavingsAccount(Account): says a savings account is an account: it gets every attribute and method of Account without repeating a line, and it can add new ones or change existing ones. Account is the parent (or base, or superclass); SavingsAccount is the child (or subclass). An instance of the child is also an instance of the parent, so isinstance(savings, Account) is True and any code written for accounts accepts it.

Overriding means defining a method in the child with the same name as one in the parent. When called on a child instance, the child's version wins. Often you want to extend rather than replace: do what the parent does, plus something. super().withdraw(amount) calls the parent's version from inside the override, so the parent's checks still run and the child adds its own behavior around them. The same applies to __init__: a child's initializer nearly always starts with super().__init__(...) to set up the inherited attributes, then adds its own.
| Situation | Write |
|---|---|
| inherit everything, add nothing | class Child(Parent): pass |
| add a new method | define it in the child |
| replace a parent method entirely | define the same name in the child |
| extend a parent method | define it, and call super().method(...) inside |
| add attributes in the child | super().__init__(...) first, then self.new = ... |
When Python looks up a method, it searches the instance's class, then its parent, then the parent's parent, up to object, the root of every class. That search order is the method resolution order, and SavingsAccount.__mro__ shows it. With single inheritance it is the chain of parents; with multiple parents Python computes a consistent order, which you will mostly meet in mixins in other people's code. The MRO explains every "which version runs?" question.

The question to ask before inheriting is is a: is a savings account an account, in every way that matters, such that any code expecting an account would be happy with a savings account? If yes, inherit. If the relationship is really has a or uses a, such as a car and its engine, the right tool is composition, covered in the last lesson of this module. Inheritance is powerful and easy to overuse; a hierarchy more than two or three levels deep is almost always a sign that composition would have been clearer.
Code
A child that inherits, adds a method, and overrides another by extending it with super(). The parent's guard still runs inside the child's version.
# savings.py
class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("insufficient funds")
self.balance -= amount
def __repr__(self):
return f"{type(self).__name__}({self.owner!r}, {self.balance})"
class SavingsAccount(Account):
def __init__(self, owner, balance=0, rate=0.02):
super().__init__(owner, balance)
self.rate = rate
def add_interest(self):
self.deposit(round(self.balance * self.rate, 2))
def withdraw(self, amount):
if amount > 500:
raise ValueError("savings withdrawals are capped at 500")
super().withdraw(amount)
s = SavingsAccount("ada", 1000, rate=0.05)
s.add_interest()
print(s)
s.withdraw(100)
print(s)
for attempt in (600, 5000):
try:
s.withdraw(attempt)
except ValueError as err:
print("refused:", err)
print(isinstance(s, Account), isinstance(s, SavingsAccount))
SavingsAccount('ada', 1050.0)
SavingsAccount('ada', 950.0)
refused: savings withdrawals are capped at 500
refused: savings withdrawals are capped at 500
True TrueThe method resolution order, printed, and a lookup that walks it. describe is found on the grandparent because neither child defines it.
# mro.py
class Animal:
def describe(self):
return f"{type(self).__name__} says {self.sound()}"
def sound(self):
return "..."
class Dog(Animal):
def sound(self):
return "woof"
class Puppy(Dog):
pass
print([cls.__name__ for cls in Puppy.__mro__])
print(Puppy().describe())
print(Animal().describe())
['Puppy', 'Dog', 'Animal', 'object']
Puppy says woof
Animal says ...Forgetting super().__init__ is the classic inheritance bug: the child sets its own attribute and never sets up the parent's, so the inherited method fails.
# forgot_super.py
class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
class Broken(Account):
def __init__(self, owner, rate):
self.rate = rate
class Fixed(Account):
def __init__(self, owner, rate):
super().__init__(owner)
self.rate = rate
print(Fixed("ada", 0.02).balance)
print(Broken("ada", 0.02).balance)
0
Traceback (most recent call last):
File "forgot_super.py", line 19, in <module>
print(Broken("ada", 0.02).balance)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'Broken' object has no attribute 'balance'Code written for the parent works with every child. The function knows nothing about savings or checking accounts, and does not need to.
# substitutable.py
class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
class SavingsAccount(Account):
kind = "savings"
class CheckingAccount(Account):
kind = "checking"
def total_balance(accounts):
return sum(a.balance for a in accounts)
accounts = [SavingsAccount("ada", 300), CheckingAccount("linus", 120), Account("grace", 80)]
print(total_balance(accounts))
print([getattr(a, "kind", "plain") for a in accounts])
500
['savings', 'checking', 'plain']Try it yourself
Create vehicles.py with a Vehicle class holding make, model, and speed with accelerate(amount) and a __repr__. Add Car(Vehicle) with a doors attribute set through super().__init__, and Bicycle(Vehicle) that overrides accelerate to cap speed at 40 by calling super() then clamping. Print the MRO of Car, then put one of each in a list and accelerate them all in one loop.
Common mistakes
- Skipping
super().__init__(). The parent's attributes never get created and the first inherited method that touches them fails. - Replacing when you meant to extend. Overriding
withdrawwithout callingsuper().withdrawsilently drops the parent's checks. - Inheriting for code reuse alone. If a
Carinherits fromEngineto reusestart(), the "is a" test fails; give the car an engine instead.
What's next
class Child(Parent):inherits everything; define a method with the same name to override it, and callsuper()to extend rather than replace.- Method lookup walks the MRO from the instance's class upward;
__mro__shows the order. - Inherit only when the child "is a" parent in every way that matters; otherwise compose.
Children can stand in for their parents. Next you will see the pillar that makes that useful: polymorphism, and Python's relaxed version of it, duck typing.