Classes and objectsChapter 64 of 114
Inheritance
Build a class on top of another, and override what differs.
A subclass gets everything
Put the parent class in brackets. The child starts with all its methods:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound"
class Dog(Animal):
pass
rex = Dog("Rex")
print(rex.speak())
print(isinstance(rex, Dog), isinstance(rex, Animal))Output
Rex makes a sound True True
A Dog is an Animal. That phrase is the test for whether inheritance is the right tool.
Overriding
Define a method with the same name and the child's version wins:
class Animal:
def speak(self):
return "some sound"
class Dog(Animal):
def speak(self):
return "woof"
class Cat(Animal):
def speak(self):
return "meow"
for animal in [Animal(), Dog(), Cat()]:
print(animal.speak())Output
some sound woof meow
super() calls the parent
Use it to extend rather than replace, especially in __init__:
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
rex = Dog("Rex", "collie")
print(rex.name, rex.breed)Output
Rex collie
Forget the super().__init__(name) and self.name never gets set, so the first use raises AttributeError.
super() works for any method, not just __init__:
class Animal:
def describe(self):
return "an animal"
class Dog(Animal):
def describe(self):
return super().describe() + ", specifically a dog"
print(Dog().describe())Output
an animal, specifically a dog
Attribute lookup order
Python looks at the object, then its class, then up the chain of parents. The order is on the class itself:
class Animal: pass
class Dog(Animal): pass
print([c.__name__ for c in Dog.__mro__])Output
['Dog', 'Animal', 'object']
Every class inherits from object eventually, which is where the default __repr__ and friends come from.
Multiple inheritance
Python allows more than one parent. The MRO decides which method wins:
class Swimmer:
def move(self):
return "swims"
class Runner:
def move(self):
return "runs"
class Dog(Runner, Swimmer):
pass
print(Dog().move())
print([c.__name__ for c in Dog.__mro__])Output
runs ['Dog', 'Runner', 'Swimmer', 'object']
Leftmost parent first. Multiple inheritance gets confusing quickly; mixins with no overlapping methods are the case where it stays manageable.
Prefer composition when it is not "is a"
A car is not an engine, so it should hold one rather than inherit from it:
class Engine:
def start(self):
return "engine running"
class Car:
def __init__(self):
self.engine = Engine()
def start(self):
return "car: " + self.engine.start()
print(Car().start())Output
car: engine running
Composition is easier to change later, because nothing depends on the shape of a class you did not write.
Test yourself
2 questionsWhat does super().__init__(name) do in a subclass?
Show the answer
Runs the parent's initialiser so its setup still happens — Skip it and the attributes the parent sets never exist, so the first use raises AttributeError.
When is composition a better fit than inheritance?
Show the answer
When the relationship is 'has a' rather than 'is a' — A car has an engine; it is not one. Composition is also easier to change later.
Polymorphism
Different types answering the same call, and why Python barely notices.