Kodewick
Lesson 27 of 31Contents

Inheritance

Lesson 27 of 31 · Part 5 · Objects · about 20 minutes

Pixel Town has houses, shops and a warehouse. They are all buildings: each has a name and a number of windows to light. With inheritance you write what they share once, in a Building class, and every other class starts from it.

main.py
1class Building:2    def __init__(self, name, windows):3        self.name = name4        self.windows = windows5 6    def describe(self):7        return f"{self.name} with {self.windows} windows"8 9class House(Building):10    pass11 12home = House("Ada's house", 4)13print(home.describe())14print(home.windows)
Output
Ada's house with 4 windows
4

Parent and child

class House(Building): makes House a child class (or subclass) of Building, its parent class (or base class). The class in the parentheses is the one it inherits from. House has no code of its own (pass means "nothing here"), yet a house gets everything a building has: __init__, describe and the attributes they set.

When you call home.describe(), Python looks for describe in House first. It's not there, so Python looks in the parent, Building, and finds it. A child can also add things the parent doesn't have:

main.py
1class Building:2    def __init__(self, name, windows):3        self.name = name4        self.windows = windows5 6class Shop(Building):7    def open_up(self):8        print(f"{self.name} is open")9 10bakery = Shop("Bakery", 6)11bakery.open_up()12town_hall = Building("Town hall", 12)13town_hall.open_up()
Output
Bakery is open
Traceback (most recent call last):
  File "main.py", line 13, in <module>
    town_hall.open_up()
AttributeError: 'Building' object has no attribute 'open_up'

A shop can open_up, but a plain building can't: methods go from parent to child, never the other way.

Overriding a method

If the child defines a method with the same name as the parent, the child's version wins for objects of the child class. That is called overriding.

Guess first

What does this print?

1class Building:2    def lights_needed(self):3        return 24 5class Warehouse(Building):6    def lights_needed(self):7        return 108 9print(Building().lights_needed(), Warehouse().lights_needed())

super()

Often the child wants to do what the parent does and a bit more. super() gives you the parent's version of a method, so you can call it instead of copying its code. It's most common in __init__:

main.py
1class Building:2    def __init__(self, name, windows):3        self.name = name4        self.windows = windows5 6    def describe(self):7        return f"{self.name} with {self.windows} windows"8 9class Shop(Building):10    def __init__(self, name, windows, goods):11        super().__init__(name, windows)12        self.goods = goods13 14    def describe(self):15        return super().describe() + f", selling {self.goods}"16 17bakery = Shop("Bakery", 6, "bread")18print(bakery.describe())
Output
Bakery with 6 windows, selling bread

Shop.__init__ lets Building.__init__ set name and windows, then adds goods. Shop.describe takes the parent's sentence and adds to its end. Press Step to watch the calls go from the child up to the parent and back; self is the same shop the whole time.

If a child's __init__ forgets to call super().__init__, the parent's never runs, and its attributes are missing:

main.py
1class Building:2    def __init__(self, name):3        self.name = name4 5class House(Building):6    def __init__(self, name, owner):7        self.owner = owner8 9home = House("Rose Cottage", "Ada")10print(home.owner)11print(home.name)
Output
Ada
Traceback (most recent call last):
  File "main.py", line 11, in <module>
    print(home.name)
AttributeError: 'House' object has no attribute 'name'

Polymorphism

Different classes can answer the same method call in their own way. Code that only calls the method doesn't need to know which class it has. That idea is called polymorphism:

main.py
1class Building:2    def __init__(self, name):3        self.name = name4 5    def lights_needed(self):6        return 27 8class House(Building):9    pass10 11class Warehouse(Building):12    def lights_needed(self):13        return 1014 15class Mine(Building):16    def lights_needed(self):17        return 2518 19street = [House("Rose Cottage"), Warehouse("Depot"), Mine("North mine")]20for b in street:21    print(b.name, b.lights_needed())22print("Total:", sum([b.lights_needed() for b in street]))
Output
Rose Cottage 2
Depot 10
North mine 25
Total: 37

The loop treats every item the same way. Add a new kind of building tomorrow, and the loop keeps working without a change.

isinstance and issubclass

isinstance(obj, Class) asks "is this object a Class?", and the answer counts child classes too: a warehouse is a building. issubclass(Child, Parent) asks the same about two classes.

main.py
1class Building:2    pass3 4class Warehouse(Building):5    pass6 7depot = Warehouse()8print(isinstance(depot, Warehouse), isinstance(depot, Building))9print(isinstance(Building(), Warehouse))10print(issubclass(Warehouse, Building), issubclass(Building, Warehouse))11print(type(depot) == Building)
Output
True True
False
True False
False

Note the last line: type() gives the exact class, so a warehouse's type is Warehouse, not Building. That's why isinstance is usually the better check. It also takes a tuple of classes: isinstance(x, (int, float)) is true for either kind of number.

Guess first

What does this print?

1class Building:2    pass3 4class Shop(Building):5    pass6 7class Bakery(Shop):8    pass9 10print(isinstance(Bakery(), Building))

Everything inherits from object

A class with no parentheses, like class Lamp:, still has a parent: Python's built-in object. Every class leads back to it, including int, str and list. So every value in Python is an object:

main.py
1class Lamp:2    pass3 4print(issubclass(Lamp, object))5print(isinstance(5, object), isinstance("ore", object))6print(issubclass(bool, int))7print(isinstance(True, int))
Output
True
True True
True
True

object gives every class a few basics, like a do-nothing __init__: that's why Lamp() works even though Lamp defines none. The last two lines are a surprise: bool is a child class of int, so True counts as an int (it behaves like 1).

When to use inheritance

Use inheritance when the child really is a kind of the parent: a warehouse is a building, a savings account is a bank account. If one thing just has another, store it as an attribute instead. A street is not a lamp; a street has lamps:

main.py
1class Lamp:2    def __init__(self, brightness):3        self.brightness = brightness4 5class Street:6    def __init__(self, name):7        self.name = name8        self.lamps = []9 10    def add(self, lamp):11        self.lamps.append(lamp)12 13mill = Street("Mill Road")14mill.add(Lamp(3))15mill.add(Lamp(5))16print(mill.name, len(mill.lamps), "lamps")
Output
Mill Road 2 lamps

Keep class families small and shallow. One or two levels, like Building and its children, are easy to follow; a long chain makes you jump between many classes to see what one method does.

Practice

Write the code, then press Check. A hint costs nothing; the answer is there when you need it.

Exercise 1

A noisy mine

Make Mine a child class of Building and override sound so it returns "clang". The program should print the two lines below.

It should print
quiet
clang
exercise1.py
1class Building:2    def sound(self):3        return "quiet"4 5class Mine:6    pass7 8print(Building().sound())9print(Mine().sound())
Output
Press Run or Ctrl+Enter
Exercise 2

A savings account

Savings.__init__ should let the parent set owner and balance, then store rate. Fix it with super() so the program prints the line below.

It should print
Wick 5.0
exercise2.py
1class Account:2    def __init__(self, owner, balance):3        self.owner = owner4        self.balance = balance5 6class Savings(Account):7    def __init__(self, owner, balance, rate):8        self.rate = rate9 10s = Savings("Wick", 100, 0.05)11print(s.owner, s.balance * s.rate)
Output
Press Run or Ctrl+Enter
Exercise 3

Only the carts

The list mixes Cart objects and other values. Print the total load of the items that are carts, using isinstance.

It should print
12
exercise3.py
1class Cart:2    def __init__(self, load):3        self.load = load4 5items = [Cart(4), "rope", Cart(7), 9, Cart(1)]6total = 07# Your code here8print(total)
Output
Press Run or Ctrl+Enter

What you learned

  • class Child(Parent): makes a child class that gets all of the parent's methods and can add its own.
  • A method in the child with the same name overrides the parent's.
  • super().method(...) calls the parent's version; a child's __init__ usually starts with super().__init__(...).
  • Polymorphism: different classes answer the same method call in their own way, so one loop handles them all.
  • isinstance and issubclass count parent classes too; type() gives only the exact class.
  • Every class inherits from object. Use inheritance for "is a"; for "has a", use an attribute.