Inheritance and super: Similar Classes Without Duplicated Code
- Explain when inheritance saves duplicated code, and check it with the “is a kind of” test
- Define a Parent and a Child, and understand how Python looks for a method — first in the Child and then in the Parent
- Override a method in the Child to change behavior
- Use
super()— both in__init__and to extend a method of the Parent
In this lesson
This lesson is built as one story, in the same order as the slides:
- Chapter 1 — why inheritance: the same code twice, and how to inherit instead of copying
- Chapter 2 — overriding and
super(): inheritance, but not exactly — changing only part of the behavior
At the end of the lesson — practice, and a mini project that connects both chapters.
Chapter 1: Why Inheritance — Parent, Child and the Is-a-Kind-of Relationship
What will we learn?
- Why inheritance is needed: the problem of almost identical Classes.
- Parent Class (Base Class) and Child Class (Subclass).
- The syntax of inheritance in Python.
- What is passed on through inheritance.
- The is-a test: when inheritance suits and when it does not.
Why do we need this?
In the lesson on classes and objects we asked: what happens when you add a “savings account” that is similar to a regular account, but with interest? In the procedural version, every function filled up with if account["type"] == .... Let’s see the same problem in OOP, without inheritance:
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def monthly_cost(self):
return self.salary * 1.25
def describe(self):
return self.name + " earns " + str(self.salary)
class Manager:
def __init__(self, name, salary, team_size):
self.name = name
self.salary = salary
self.team_size = team_size
def monthly_cost(self):
return self.salary * 1.25
def describe(self):
return self.name + " earns " + str(self.salary)
def team_report(self):
return self.name + " manages " + str(self.team_size) + " people"Manager copies almost all of Employee: name, salary, monthly_cost, describe. What happens when the employer cost changes from 1.25 to 1.3? You have to fix it in two places, and if there are also Intern, Contractor and Director, in five. This is a violation of DRY at the level of Classes.
And one more thing: a manager is an employee. Everything that is true for an employee is also true for a manager, and in addition the manager has more things. The code should reflect this relationship.
Explanation from the basics
Inheritance
Inheritance = a mechanism in which a new Class automatically receives all the Attributes and methods of an existing Class, and can add or change parts.
- Parent Class (Base, Superclass) — the general Class.
Employee. - Child Class (Derived, Subclass) — the specific Class, which inherits.
Manager.
We have a production template of a “car” (the Parent): 4 wheels, an engine, steering, braking. Now we want a “sports car”. We don’t design a car from scratch. We take the existing template, and add a spoiler and change the engine. Every sports car is a car (it drives, it brakes), but not every car is a sports car.
The “is a kind of” relationship (is-a)
Inheritance suits only when you can honestly say: “the Child is a kind of the Parent”.
| Sentence | Inheritance? |
|---|---|
| A manager is a kind of employee | ✓ Manager(Employee) |
| A dog is a kind of animal | ✓ Dog(Animal) |
| A savings account is a kind of bank account | ✓ SavingsAccount(BankAccount) |
| A car is a kind of engine? | ✗ A car has an engine (has-a), and that is Composition, the OOP design topic |
| An order is a kind of customer? | ✗ An order has a customer |
What is passed on through inheritance?
The Child receives everything from the Parent:
- The
__init__(if the Child does not define an__init__of its own). - All the methods.
- The Class Attributes.
And the Child can:
- Add new methods and Attributes.
- Change (Override) existing methods. We will learn it in the next chapter, together with
super().
Lookup in layers — again
When you write manager.describe(), Python looks: (1) on the object, (2) in its Class (Manager), (3) in the Parent (Employee)… and stops at the first place where it found it. This is the same idea of a layered lookup that we saw in LEGB and in the lookup order of Attributes.
Syntax
class Parent:
def __init__(self, x):
self.x = x
def shared_method(self):
return ...
class Child(Parent): # Child inherits from Parent
def extra_method(self): # new method, only in Child
return ...
c = Child(5) # uses Parent.__init__ (Child has none of its own)
c.shared_method() # inherited
c.extra_method() # Child's own- The Parent’s name comes in brackets after the Child’s name.
- A Child without any addition:
class Child(Parent): passis a valid Class that is identical to the Parent.
First code example
week-3/employees.py:
class Employee:
EMPLOYER_COST_RATE = 1.25
def __init__(self, name, salary):
self.name = name
self.salary = salary
def monthly_cost(self):
return self.salary * Employee.EMPLOYER_COST_RATE
def describe(self):
return self.name + " earns " + str(self.salary)
class Manager(Employee):
def team_report(self, team_size):
return self.name + " manages " + str(team_size) + " people"
dana = Employee("Dana", 12000)
avi = Manager("Avi", 20000)
print(dana.describe())
print(avi.describe())
print(avi.monthly_cost())
print(avi.team_report(5))Output:
Dana earns 12000
Avi earns 20000
25000.0
Avi manages 5 peopleBreaking down the code line by line
| Code | Explanation |
|---|---|
class Manager(Employee): | Manager inherits from Employee. |
Manager("Avi", 20000) | Manager has no __init__ of its own, so Python finds the one of Employee and uses it. Avi gets a name and a salary. |
avi.describe() | There is no describe in Manager. Python goes up to Employee and finds it. self is still Avi. |
avi.monthly_cost() | The same, and EMPLOYER_COST_RATE is also found through the Parent. |
avi.team_report(5) | A method that exists only in Manager. |
dana.team_report(...) | Would throw an AttributeError: a regular employee does not inherit from the manager. Inheritance goes from top to bottom only. |
And the fix of 1.25 → 1.3? You change one line, in Employee, and all the managers (and in the future, every kind of employee) are updated.
Another example
A hierarchy with several children: Animal / Dog / Cat. week-3/animals.py:
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
def birthday(self):
self.age = self.age + 1
def describe(self):
return self.name + " (" + str(self.age) + " years old)"
class Dog(Animal):
def fetch(self):
return self.name + " brings back the ball"
class Cat(Animal):
def climb(self):
return self.name + " climbs the tree"
rex = Dog("Rex", 3)
mitzi = Cat("Mitzi", 5)
rex.birthday()
print(rex.describe(), "|", rex.fetch())
print(mitzi.describe(), "|", mitzi.climb())
for animal in [rex, mitzi]:
print(type(animal).__name__, "->", animal.describe())Output:
Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the tree
Dog -> Rex (4 years old)
Cat -> Mitzi (5 years old)Breakdown:
- Two children of the same Parent:
DogandCatshare everything that is common (name,age,birthday,describe), and each one adds what is unique to it. rex.birthday()— a method written once inAnimal, which changes rex’s State.- In the loop,
animal.describe()works on every kind ofAnimal, without checking which kind it is. This is a hint of Polymorphism, which we will go deeper into in the next lesson. type(animal).__name__— the Class’s name as text.
Animal
/ \
Dog CatSummary
- Inheritance = a Child Class receives all the Attributes and methods of a Parent Class, and can add and change.
- Syntax:
class Child(Parent):. - It solves duplication between similar Classes (DRY), and reflects the “is a kind of” relationship (is-a).
- Python looks for a method in the object’s Class, and if it did not find it, goes up to the Parent.
- Inheritance is one-directional: the Parent does not know what was added in the Child.
- Do not inherit when the relationship is “has a” (has-a). Then you use Composition (the OOP design topic).
Official documentation
Chapter 2: Overriding and super() — Changing Only What Is Different
What will we learn?
super(): how a Child uses the Parent’s code.__init__in a Child: adding Attributes without copying the Parent’s.- Method Overriding: a Child replaces a method of the Parent.
- Extending: a Child adds to the Parent’s method, instead of replacing it completely.
Why do we need this?
This is where plain inheritance gets stuck: a Truck that inherits from a Vehicle needs an extra Attribute (cargo_kg), meaning an __init__ of its own. But if we write a new __init__ in Truck, it replaces the one of Vehicle, and then brand and wheels will not be initialized, unless we copy the code. And that is exactly what inheritance came to prevent.
And a second problem: sometimes the Child needs to behave differently in the same action. A manager gets a bonus in the cost calculation. A savings account does not allow withdrawing below a minimum. We need a way to change a method that came through inheritance.
Explanation from the basics
Method Overriding
If in the Child you define a method with the same name as in the Parent, the Child’s version wins for objects of type Child. This is called an Override. Why? Remember the lookup order: Python looks first in the object’s Class, and only if it did not find it, goes up to the Parent. If the method exists in the Child, Python never even reaches the Parent’s version.
super() — “the Parent’s version”
super() returns a special object through which you call the Parent’s method, on the same self. The two main uses:
- In
__init__:super().__init__(...)— “first initialize everything the Parent initializes”, and then add what is unique to the Child. - In extending a method:
super().describe()— “bring me what the Parent returns”, and then add to it.
Grandma (the Parent) bakes a chocolate cake from a recipe. You (the Child) inherit the recipe. Override = you decide to bake a completely different cake with the same name, “chocolate cake”, but vegan, from scratch. Extending with super() = “do exactly like grandma” (super().bake()), and then add a frosting you invented. You don’t copy grandma’s whole recipe. You simply say “like grandma, plus…”.
Three options for a method in a Child
| Option | What you do | When |
|---|---|---|
| Inherit | You write nothing | The Parent’s behavior suits |
| Override | You write a method with the same name, without super() | The behavior is completely different |
| Extend | You write a method with the same name, and inside it super().method() | “Like the Parent, plus something” |
Syntax
class Parent:
def __init__(self, a):
self.a = a
def describe(self):
return "a=" + str(self.a)
def greet(self):
return "Hello from Parent"
class Child(Parent):
def __init__(self, a, b):
super().__init__(a) # let Parent set up self.a
self.b = b # then add what is new in Child
def describe(self): # EXTEND: reuse Parent's result
return super().describe() + ", b=" + str(self.b)
def greet(self): # OVERRIDE: replace completely
return "Hello from Child"Notice: in super().__init__(a) you do not pass self. super() already “knows” which object it is about.
First code example
Solving the Truck problem. week-3/vehicles_super.py:
class Vehicle:
def __init__(self, brand, wheels):
self.brand = brand
self.wheels = wheels
def describe(self):
return self.brand + " with " + str(self.wheels) + " wheels"
class Truck(Vehicle):
def __init__(self, brand, max_load_kg):
super().__init__(brand, 6)
self.max_load_kg = max_load_kg
self.cargo_kg = 0
def load(self, kg):
if self.cargo_kg + kg > self.max_load_kg:
raise ValueError("over the limit: max " + str(self.max_load_kg) + " kg")
self.cargo_kg = self.cargo_kg + kg
def describe(self):
return super().describe() + ", carrying " + str(self.cargo_kg) + "/" + str(self.max_load_kg) + " kg"
volvo = Truck("Volvo", 1000)
volvo.load(600)
print(volvo.describe())
try:
volvo.load(500)
except ValueError as error:
print("Rejected:", error)
print(volvo.brand, volvo.wheels)Output:
Volvo with 6 wheels, carrying 600/1000 kg
Rejected: over the limit: max 1000 kg
Volvo 6Breaking down the code line by line
| Code | Explanation |
|---|---|
def __init__(self, brand, max_load_kg): | Truck has an __init__ of its own, with parameters that suit a truck. |
super().__init__(brand, 6) | “Run Vehicle’s __init__ on this object”. This creates self.brand and self.wheels. A truck always has 6 wheels, so the Child decides the value. |
self.max_load_kg = ..., self.cargo_kg = 0 | Only what is new in Truck. Without copying a single line from Vehicle. |
def describe(self): in Truck | Extending: the same name as in Vehicle. |
super().describe() | Gets "Volvo with 6 wheels" from the Parent, and adds the cargo details to it. |
volvo.brand, volvo.wheels | They exist, because super().__init__ created them. |
If Vehicle changes tomorrow, for example adds self.year, trucks will get it automatically through super().__init__. If we had copied the code, we would need to remember to update Truck too.
Another example
A full Override, extending, and super() inside business rules. week-3/staff.py:
class Employee:
EMPLOYER_COST_RATE = 1.25
def __init__(self, name, salary):
self.name = name
self.salary = salary
def monthly_cost(self):
return self.salary * Employee.EMPLOYER_COST_RATE
def title(self):
return "Employee"
def describe(self):
return self.title() + " " + self.name + " - cost " + str(self.monthly_cost())
class Manager(Employee):
def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self.bonus = bonus
def monthly_cost(self):
return super().monthly_cost() + self.bonus
def title(self):
return "Manager"
class Intern(Employee):
def monthly_cost(self):
return self.salary
def title(self):
return "Intern"
staff = [Employee("Dana", 12000), Manager("Avi", 20000, 3000), Intern("Noa", 5000)]
for person in staff:
print(person.describe())
print("Total monthly cost:", sum(person.monthly_cost() for person in staff))Output:
Employee Dana - cost 15000.0
Manager Avi - cost 28000.0
Intern Noa - cost 5000
Total monthly cost: 48000.0Breakdown:
Manager.monthly_cost— extending: “like a regular employee (super().monthly_cost()= 25,000), plus a bonus”. If the employer cost rate changes, the managers will get the change automatically.Intern.monthly_cost— a full Override: an intern does not carry employer costs, so a completely different calculation, withoutsuper().title()— every Class overrides it.- The interesting point:
describe()is written only once, inEmployee. It callsself.title()andself.monthly_cost(). When the object is a Manager,self.monthly_cost()reaches the Manager’s version! The Parent’s method automatically uses the Child’s overridden versions. This is the basis of Polymorphism (the next lesson). Interndid not define an__init__, so it uses the one ofEmployee(regular inheritance).
Summary
- Override = a method in the Child with the same name as in the Parent. The Child’s version wins.
super()= access to the Parent’s version, on the sameself.- A Child with an
__init__of its own must callsuper().__init__(...)(withoutself), and then add only what is new. - Extending:
super().method()+ an addition. A full override: withoutsuper(). self.method()inside an overridden method = recursion. For the Parent’s version:super().method().- A Parent’s method that calls
self.x()will automatically reach the Child’s overridden version. - An overridden method keeps the same signature (the same parameters).
Official documentation
Practice
The exercises use only what was taught in this lesson and the lessons before it. Try on your own first, and only then open the solution.
What you practice on this page: inheritance — Parent and Child, the “is a kind of” test, extending __init__ with super(), and overriding a method (Override).
By the end of the page you will know how to:
- Write a Child that inherits from a Parent and adds only what is new
- Decide when inheritance suits, using the “is a kind of” test
- Call
super().__init__so that the Parent initializes its part - Override a method, and use the Parent’s version inside it
Exercise 1: An employee and a manager
What you build: an employee and a manager — where a manager is a kind of employee, so you don’t write again what is shared.
What to write:
- A Class named
Employeewith a name and a salary. - In
Employee:describethat returns the name and the salary, as in the output. - A Class named
Managerthat inherits fromEmployee— without an__init__of its own. - In
Manageronly:managethat receives a team size and returns a sentence as in the output.
Test code:
dana = Employee("Dana", 12000)
avi = Manager("Avi", 20000)
print(dana.describe())
print(avi.describe())
print(avi.manage(5))The desired output:
Dana: 12000
Avi: 20000
Avi manages 5 peopleShow solution
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def describe(self):
return self.name + ": " + str(self.salary)
class Manager(Employee):
def manage(self, team_size):
return self.name + " manages " + str(team_size) + " people"
dana = Employee("Dana", 12000)
avi = Manager("Avi", 20000)
print(dana.describe())
print(avi.describe())
print(avi.manage(5))Dana: 12000
Avi: 20000
Avi manages 5 peopleManager inherits __init__ and describe — without writing them again — and adds only what is new.
Exercise 2: is-a — is a kind of
What you do: decide — without code — when inheritance suits.
The task: for each pair, answer: should the first inherit from the second? Explain with the sentence “X is a kind of Y” — and if the sentence is not true, what the relationship between them actually is.
DogandAnimalCarandEngineSavingsAccountandBankAccountSquareandColor
What to submit: yes or no for each pair, and one sentence of explanation.
Show solution
- Yes — a dog is a kind of animal.
- No — a car is not a kind of engine. A car has an engine (has-a). This is a case for composition, not for inheritance.
- Yes — a savings account is a kind of bank account.
- No — a square is not a kind of color. Maybe a square has a color.
Inheritance only when the sentence “X is a kind of Y” is true.
Exercise 3: An animal tree
What you build: a small inheritance tree — an animal, and two kinds of animals, each with an action of its own.
What to write:
- A Class named
Animalwith a name and an age, anddescribethat returns the name and the age as in the output. - A Class named
Dogthat inherits fromAnimal, withfetchthat returns the sentence from the output. - A Class named
Catthat inherits fromAnimal, withclimbthat returns the sentence from the output.
Test code:
rex = Dog("Rex", 4)
mitzi = Cat("Mitzi", 5)
print(rex.describe(), "|", rex.fetch())
print(mitzi.describe(), "|", mitzi.climb())The desired output:
Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the treeShow solution
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
def describe(self):
return self.name + " (" + str(self.age) + " years old)"
class Dog(Animal):
def fetch(self):
return self.name + " brings back the ball"
class Cat(Animal):
def climb(self):
return self.name + " climbs the tree"
rex = Dog("Rex", 4)
mitzi = Cat("Mitzi", 5)
print(rex.describe(), "|", rex.fetch())
print(mitzi.describe(), "|", mitzi.climb())Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the treeExercise 4: super().init
What you build: a truck — a vehicle that always has 6 wheels, and also has a maximum cargo weight.
What to write:
- A Class named
Vehiclewith a brand and a number of wheels. - A Class named
Truckthat inherits fromVehicle. Its__init__receives a brand and a maximum weight. - Inside the
__init__ofTruck: callsuper().__init__with the brand and 6 wheels, and initialize a current weight of 0. load— receives kilograms and adds them to the weight. If the maximum is exceeded: aValueErrorwith the message from the output.
Test code:
truck = Truck("Volvo", 1000)
truck.load(600)
print(truck.brand, truck.wheels, truck.load_kg)
try:
truck.load(500)
except ValueError as error:
print("error:", error)The desired output:
Volvo 6 600
error: overloaded: max 1000 kgShow solution
class Vehicle:
def __init__(self, brand, wheels):
self.brand = brand
self.wheels = wheels
class Truck(Vehicle):
def __init__(self, brand, max_load_kg):
super().__init__(brand, 6)
self.max_load_kg = max_load_kg
self.load_kg = 0
def load(self, kg):
if self.load_kg + kg > self.max_load_kg:
raise ValueError("overloaded: max " + str(self.max_load_kg) + " kg")
self.load_kg = self.load_kg + kg
truck = Truck("Volvo", 1000)
truck.load(600)
print(truck.brand, truck.wheels, truck.load_kg)
try:
truck.load(500)
except ValueError as error:
print("error:", error)Volvo 6 600
error: overloaded: max 1000 kgsuper().__init__(brand, 6) initializes the Parent’s part. After that the Child adds its own Attributes.
Exercise 5: Override with super
What you build: a manager whose monthly cost is the salary plus a bonus.
What to write:
- A Class named
Employeewith a name and a salary, andmonthly_costthat returns the salary. - A Class named
Managerthat inherits fromEmployee, and also receives a bonus. In its__init__, callsuper().__init__. - In
Manager, overridemonthly_cost: the Parent’s version, throughsuper(), plus the bonus.
Test code:
print(Employee("Dana", 12000).monthly_cost())
print(Manager("Avi", 20000, 3000).monthly_cost())The desired output:
12000
23000Show solution
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def monthly_cost(self):
return self.salary
class Manager(Employee):
def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self.bonus = bonus
def monthly_cost(self):
return super().monthly_cost() + self.bonus
print(Employee("Dana", 12000).monthly_cost())
print(Manager("Avi", 20000, 3000).monthly_cost())12000
23000super().monthly_cost() calls the Parent’s version. If the calculation of the base salary changes, Manager will get the change automatically.
Mini project: Employees in a company
Which system you build: an employee system for a company. The company has regular employees, managers and interns. All of them are employees — they have a name and a salary — but each kind is displayed differently, and a manager also has a bonus. The system displays every employee and calculates the monthly cost of all of them together.
What you will learn from the project:
- Build a Parent with everything that is shared, and Children that add only what is different
- Call
super().__init__from the Child - Override a method, and extend a method with
super() - See how a method written once in the Parent works correctly for every kind
What to build — step by step:
Step 1 — an employee (Employee). __init__ receives a name and a salary. Three methods:
title— returns the word Employee;monthly_cost— returns the salary;describe— returns the name, the role and the monthly cost, as in the output. It usestitleandmonthly_cost.
Step 2 — a manager (Manager). Inherits from Employee and also receives a bonus. In __init__, call super().__init__ and save the bonus. Override title — the word Manager. Extend monthly_cost: the result of super() plus the bonus.
Step 3 — an intern (Intern). Inherits from Employee and also receives a number of internship months. In __init__, check that the number is between 1 and 12 — otherwise ValueError — and then call super().__init__. Override title — the word Intern. Do not write monthly_cost and describe: they come through inheritance.
The error message — exactly like this:
months must be 1-12Test code:
staff = [
Employee("Dana", 12000),
Manager("Avi", 20000, 3000),
Intern("Noa", 6000, 6),
]
for person in staff:
print(person.describe())
try:
Intern("Tom", 5000, 0)
except ValueError as error:
print("error:", error)
total = 0
for person in staff:
total = total + person.monthly_cost()
print("Total monthly cost:", total)The desired output:
Dana (Employee): 12000
Avi (Manager): 23000
Noa (Intern): 6000
error: months must be 1-12
Total monthly cost: 41000By the end of the project you will know how to:
- Design a hierarchy: what goes up to the Parent and what stays in the Child
- Tell apart three options for a method in a Child: inherit, override, or extend with
super() - Understand why
describeshows every employee their own role and cost, even though it was written once
Bonus: add to Manager a method manage that receives a team size and returns a sentence like Avi manages 5 people, and print it only for the managers in the list.
Show solution
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def title(self):
return "Employee"
def monthly_cost(self):
return self.salary
def describe(self):
return self.name + " (" + self.title() + "): " + str(self.monthly_cost())
class Manager(Employee):
def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self.bonus = bonus
def title(self):
return "Manager"
def monthly_cost(self):
return super().monthly_cost() + self.bonus
class Intern(Employee):
def __init__(self, name, salary, months):
if months < 1 or months > 12:
raise ValueError("months must be 1-12")
super().__init__(name, salary)
self.months = months
def title(self):
return "Intern"
staff = [
Employee("Dana", 12000),
Manager("Avi", 20000, 3000),
Intern("Noa", 6000, 6),
]
for person in staff:
print(person.describe())
try:
Intern("Tom", 5000, 0)
except ValueError as error:
print("error:", error)
total = 0
for person in staff:
total = total + person.monthly_cost()
print("Total monthly cost:", total)Dana (Employee): 12000
Avi (Manager): 23000
Noa (Intern): 6000
error: months must be 1-12
Total monthly cost: 41000describewas written once inEmployee, but every employee gets their owntitle()andmonthly_cost()— because Python looks for the method first in the Child.Manager.monthly_costextends:super().monthly_cost()returns the salary, and the bonus is added.titleoverrides completely.Interndoes not writemonthly_cost— it inherits it as it is.
Bonus:
def manage(self, team_size):
return self.name + " manages " + str(team_size) + " people"(A method inside Manager.)
for person in staff:
if person.title() == "Manager":
print(person.manage(5))Avi manages 5 people