Grade by Grade
C. Designing Modular Programs · Modular and Object-Oriented Design

Inheritance as a Design Tool: Generalization and Hierarchies

A2.2 C1.4About 70 minutes
Learning goals
  • I can write a subclass that inherits from a superclass and adds its own attributes and methods.
  • I can call the superclass constructor with super().__init__(...) and explain what goes wrong if I forget.
  • I can predict which method runs by following the method resolution order.
  • I can use the "is a" test to decide between inheritance and composition ("has a").
  • I can generalize two similar classes by moving their shared code into a new superclass.

Words to know

inheritance
A way to define a new class from an existing one, so the new class automatically has the existing class's attributes and methods.
superclass (base class, parent)
The class being inherited from.
subclass (derived class, child)
The new class that inherits; it can add attributes and methods or change inherited ones.
super()
A built-in that gives access to the superclass's methods, most often super().__init__(...).
is-a relationship
The test for inheritance: every Skis object is an Item, so Skis may inherit from Item.
has-a relationship
One object contains or uses another (a Shop has Items). Modelled with attributes (composition), not inheritance.
generalization
Moving common attributes and methods from several classes up into a shared superclass.
method resolution order (MRO)
The order in which Python searches a class and its superclasses for a method or attribute.

The copy-and-paste trap

The rental shop now needs different kinds of equipment. Skis have a length. Helmets have a size and must be cleaned after each use. Snowboards have a stance. Every kind also needs an ID, a daily rate, availability and a cost calculation.

You could copy the Item class three times and edit each copy. Then a change to the cost rule (say, a weekly discount) must be made in three places, and sooner or later one copy gets missed. Inheritance solves this: write the shared part once in a superclass and let each subclass inherit it and add only what is different. Expectation A2.2 names inheritance as one of the core concepts for reusable code.

Remember from Data Types, Files and Expressions

In Type Conversion in an Inheritance Hierarchy you read a Vehicle hierarchy with Bus and Train subclasses, used super().__init__, met the is-a relationship, and used isinstance to treat a mixed list safely (upcasting and downcasting). This lesson is about designing hierarchies: deciding what goes in the superclass, when inheritance is the right tool, and what goes wrong when it is not.

Writing a subclass: a quick recap

  • Put the superclass name in brackets: class Skis(Item): reads "Skis is a kind of Item".
  • In the subclass constructor, call super().__init__(...) first, passing what the superclass needs. That runs Item's constructor on the new object, so the shared attributes are set up in one place.
  • Then set the subclass's own attributes (self.length_cm) and define any new methods (suits).
  • Everything you do not define is inherited: a Skis object can call cost() and label() even though the Skis class never mentions them.

Inside label, type(self).__name__ gives the name of the object's actual class, so the same inherited method prints "Skis" for skis and "Helmet" for helmets.

Item+item_id: str+daily_cents: int+available: bool+cost(days): int+label(): strSkis+length_cm: int+suits(height_cm): boolHelmet+size: str+sanitized: bool+mark_used()is a
An inheritance hierarchy in UML. The hollow triangle points at the superclass. Skis and Helmet list only what they add; they also have everything in Item (item_id, daily_cents, available, cost, label) without repeating it.
Worked example

Example 1: An equipment hierarchy

Write Skis and Helmet as subclasses of Item. Skis add a length and a suits(height_cm) method using this shop's rule of thumb (a ski from 20 cm shorter than the skier up to the skier's height). Helmets add a size and a sanitized flag. Test that inherited methods work.

  1. Item has the shared attributes (item_id, daily_cents, available) and methods (cost, label).
  2. Skis.__init__ calls super().__init__(item_id, daily_cents) and then stores length_cm. suits uses a chained comparison: 152 ≤ 160 ≤ 172 is True for a 172 cm skier.
  3. Helmet.__init__ does the same, then adds size and sanitized. mark_used() sets sanitized to False.
  4. s.cost(3) runs the inherited Item.cost: 3500 × 3 = 10 500.
  5. isinstance(h, Item) is True (a Helmet is an Item); isinstance(h, Skis) is False.
  6. Skis.__mro__ lists the search order: Skis, then Item, then object, the root class of every Python class.

Code and output below. Notice how little the subclasses contain: only their differences.

class Item:
"""Any piece of rental equipment."""
def __init__(self, item_id, daily_cents):
self.item_id = item_id
self.daily_cents = daily_cents
self.available = True
def cost(self, days):
"""Return the rental cost in cents."""
return self.daily_cents * days
def label(self):
return f"{self.item_id} ({type(self).__name__})"
class Skis(Item):
"""Skis: an Item that also has a length."""
def __init__(self, item_id, daily_cents, length_cm):
super().__init__(item_id, daily_cents) # Item sets up the shared part
self.length_cm = length_cm
def suits(self, height_cm):
"""This shop's rule of thumb: skis from height - 20 cm up to height."""
return height_cm - 20 <= self.length_cm <= height_cm
class Helmet(Item):
"""A helmet: an Item with a size that must be cleaned after use."""
def __init__(self, item_id, daily_cents, size):
super().__init__(item_id, daily_cents)
self.size = size
self.sanitized = True
def mark_used(self):
self.sanitized = False
s = Skis("S1", 3500, 160)
h = Helmet("H1", 800, "M")
print(s.label(), s.cost(3), s.suits(172))
print(h.label(), h.cost(3), h.sanitized)
h.mark_used()
print(h.sanitized, isinstance(h, Item), isinstance(h, Skis))
print([c.__name__ for c in Skis.__mro__])

equipment.py: one superclass and two subclasses.

S1 (Skis) 10500 True
H1 (Helmet) 2400 True
False True False
['Skis', 'Item', 'object']

Output of the program above (what Python prints).

s.cost(3)Skis__init__, suitsnot hereItem__init__, cost, labelfound: run Item.costobject__str__, __eq__, ...(not needed)Python searches the object's class first, then each superclass in order (the MRO), and runs the first match.
How s.cost(3) is found. Skis does not define cost, so Python moves up to Item, finds it, and runs it with self = the Skis object. Every Python class ultimately inherits from the built-in class object.

Forgetting super().__init__

If a subclass defines its own __init__, Python runs only that one. It does not automatically run the superclass constructor too. Forget the super().__init__(...) line and the inherited attributes are never created, so the first inherited method that uses them fails.

class Item:
"""Any piece of rental equipment."""
def __init__(self, item_id, daily_cents):
self.item_id = item_id
self.daily_cents = daily_cents
self.available = True
def cost(self, days):
"""Return the rental cost in cents."""
return self.daily_cents * days
def label(self):
return f"{self.item_id} ({type(self).__name__})"
class Snowboard(Item):
def __init__(self, item_id, daily_cents, stance):
self.stance = stance # BUG: forgot super().__init__(...)
b = Snowboard("B1", 4000, "regular")
try:
print(b.cost(2))
except AttributeError as err:
print("AttributeError:", err)

A common bug. The Snowboard object has a stance, but no daily_cents, because Item.__init__ never ran. Output: AttributeError: 'Snowboard' object has no attribute 'daily_cents'

Is-a or has-a?

Use inheritance only when the subclass truly is a kind of the superclass, everywhere the superclass is used. A useful check is the substitution test: could you hand a Skis object to any code that expects an Item and have everything still make sense? Yes, so inheritance fits.

By contrast, a Shop has Items; it is not a kind of Item. A Car has an Engine; it is not an Engine. "Has-a" relationships are modelled with attributes (composition), as you did in the CRC lesson. Using inheritance for has-a relationships is a common design mistake: the subclass ends up with methods that make no sense for it (imagine shop.cost(3)).

The principle behind the substitution test is named after computer scientist Barbara Liskov: the Liskov substitution principle. Objects of a subclass should be usable wherever objects of the superclass are expected.

Worked example

Example 2: Generalizing two classes

A school app has a Student class and a Teacher class. Both store a name and an email and have identical contact() methods; Students also have a grade and marks, and Teachers have a subject. Remove the duplication.

  1. Find what is common: name, email, contact(). That becomes a new superclass, Person.
  2. Find what is different: Student keeps grade, marks and add_mark; Teacher keeps subject.
  3. Each subclass constructor passes name and email up with super().__init__(name, email).
  4. Check the is-a test: a Student is a Person and a Teacher is a Person. Good.
  5. The loop calls p.contact() on a mixed list. It works for both kinds because both inherit the same method.

The generalized code is below. If the email format ever changes, only Person.contact changes.

class Person:
"""Anyone at the school."""
def __init__(self, name, email):
self.name = name
self.email = email
def contact(self):
return f"{self.name} <{self.email}>"
class Student(Person):
def __init__(self, name, email, grade):
super().__init__(name, email)
self.grade = grade
self.marks = []
def add_mark(self, mark):
self.marks.append(mark)
class Teacher(Person):
def __init__(self, name, email, subject):
super().__init__(name, email)
self.subject = subject
noor = Student("Noor", "noor@school.example", 12)
noor.add_mark(91)
mr_t = Teacher("Mr. Tremblay", "tremblay@school.example", "Computer Science")
for p in [noor, mr_t]:
print(p.contact())
print(noor.marks, mr_t.subject)

people.py: Person is the generalization of Student and Teacher. Emails use the reserved .example domain.

Noor <noor@school.example>
Mr. Tremblay <tremblay@school.example>
[91] Computer Science

Output of the program above (what Python prints).

Good hierarchy habits

  • Keep hierarchies shallow: two or three levels are usually enough. Deep chains are hard to follow.
  • Put a method as high in the hierarchy as it makes sense for every subclass, and no higher.
  • Call super().__init__ first in a subclass constructor, so the shared setup is done before you add to it.
  • Do not inherit just to reuse one helper method. If the is-a test fails, use composition or a plain function in a module instead.
Just for fun

A subclass phones its superclass: "Thanks for everything you gave me. I promise to use it well."

The superclass replies: "Just remember to call. Specifically, super().__init__."

Hands-on activity

Build a family tree of classes

You need: paper, a pen, and a computer with Python.

  1. Pick a category you know well: vehicles at a local transit agency (bus, streetcar, ferry), musical instruments, or animals at a wildlife sanctuary.
  2. Draw a UML hierarchy with one superclass and at least three subclasses. List the shared attributes and methods in the superclass and only the differences in each subclass.
  3. Apply the is-a test to every arrow, aloud: "A ferry is a vehicle." Then think of one has-a relationship in the same domain (a bus has a driver) and model it as an attribute instead.
  4. Write the classes. Every subclass constructor must call super().__init__. Create one object of each subclass, put them in a list, and call an inherited method on each in a loop.
  5. Delete one super().__init__ line on purpose, run the program, and read the error. Put it back.

What to look for: no repeated code between subclasses, a working loop over the mixed list, and an AttributeError in step 5 that names the missing superclass attribute.

Your turn

1

In class Helmet(Item):, which class is the superclass and which is the subclass?

2

Name everything a Helmet object can do or knows, including inherited members.

3

Predict the output:

s = Skis("S9", 3000, 150)

print(s.label(), s.cost(2), s.suits(180))

4

What does super().__init__(item_id, daily_cents) do?

5

Apply the is-a test: should Classroom inherit from Desk? Should Laptop inherit from Computer?

6

Write a subclass Snowboard(Item) with a stance attribute ("regular" or "goofy") and correct use of super.

7

What is the MRO of Helmet?

More practice: warm-up

1

Give two other names for a superclass.

2

How do you write that class Dog inherits from class Animal?

3

What built-in class does every Python class inherit from in the end?

4

Which function checks whether an object belongs to a class or any of its subclasses?

5

Is "a car has an engine" an is-a or a has-a relationship?

More practice: on your own

1

Predict the output:

class A:

    def hi(self):

        return "A"

class B(A):

    pass

class C(B):

    def hi(self):

        return "C"

print(B().hi(), C().hi())

2

Add a method weekly_cost(self) to Item that charges 6 days for a 7-day rental. Which subclasses get it, and what is it for skis S1 at 3500 cents a day?

3

A student writes class Shop(Item): so the shop can reuse label(). Explain the design problem.

4

Generalize: Bus has route, capacity and describe(); Ferry has capacity, vehicle_spaces and describe(). Both describe() methods print the capacity. Sketch the superclass.

5

Why should a subclass constructor usually call super().__init__ before setting its own attributes?

Challenge

1

Predict the output, and explain the order of the lines:

class Base:

    def __init__(self):

        print("Base init")

class Mid(Base):

    def __init__(self):

        print("Mid start")

        super().__init__()

        print("Mid end")

class Leaf(Mid):

    pass

Leaf()

2

A square is a rectangle in math. A Rectangle class has set_width(w) and set_height(h) that change one side each. Explain why making Square(Rectangle) can break the Liskov substitution principle.

3

Design a hierarchy for an Ontario Parks booking app: campsites, roofed accommodations (cabins, yurts) and backcountry sites. Give the superclass's attributes and methods, at least one middle level, and justify one place where you chose not to add a subclass.

Check your understanding

Quiz

1.

What does class Helmet(Item): mean?

2.

A subclass defines its own __init__ but forgets super().__init__(...). What happens?

3.

Which pair is the best use of inheritance?

4.

For class Skis(Item), where does Python look first for s.cost?

5.

What is isinstance(Skis("S1", 3500, 160), Item)?

6.

What is generalization?

For parents and teachers

Big idea: A2.2 names inheritance as a core concept for reusable code, and C1.4 asks students to apply reuse through classes and inheritance. A subclass gets everything from its superclass and adds only what is different. The next lesson covers changing inherited behaviour (overriding) and treating different subclasses the same way (polymorphism).

How to help: Ask "Is a ___ really a kind of ___?" about each class your teen designs. If the answer is "it has one", inheritance is the wrong tool.

Common difficulties:

  • Forgetting super().__init__(...), which causes confusing AttributeErrors later.
  • Using inheritance for has-a relationships.
  • Repeating superclass code in each subclass instead of relying on inheritance.
  • Building very deep hierarchies that are hard to follow.