Learn
06.3 | INHERITANCE AND POLYMORPHISM
01 | A SPECIALISED KIND OF OBJECT
A delivery robot is a kind of robot with extra information about its destination. Inheritance lets DeliveryRobot reuse and specialise behaviour from Robot. Robot is the base class; DeliveryRobot is a subclass.
The class header class DeliveryRobot(Robot): states that relationship. An instance can use methods found on its class or inherited from its base classes. This is an is-a design, not merely a convenient way to copy code.
A library has books but is not a book. That relationship is better represented by composition, which the next lesson explores. Choosing the relationship before the syntax prevents an unnecessarily tangled hierarchy.
02 | REUSE A BASE METHOD
The base Robot initialises a name and provides describe(). DeliveryRobot initially inherits both without redefining them. A call on the subclass instance finds the inherited method and uses that instance as self.
isinstance(delivery, Robot) is true because a DeliveryRobot instance is also an instance of its base type. This does not automatically prove the subclass preserves every behavioural promise you intended.
The subclass below uses pass because its body needs a statement even when it adds nothing yet. It demonstrates inheritance, not a practical reason to create an otherwise identical type.
class Robot:
def __init__(self, name):
self.name = name
def describe(self):
return f"Robot {self.name}"
class DeliveryRobot(Robot):
pass
delivery = DeliveryRobot("Courier")
print(delivery.describe())
print(isinstance(delivery, Robot))EXPECTED OUTPUT
Robot Courier True
03 | EXTEND INITIALISATION WITH SUPER
When a subclass defines its own __init__, it must arrange any base initialisation it relies on. super().__init__(name) delegates that part through Python’s method-resolution order. In this simple single-inheritance design, it calls Robot’s initializer.
super() does not mean create another separate Robot object. It finds the next implementation for the same current instance. The base method assigns name on the delivery instance, then the subclass assigns destination.
If you omit the base initialisation and later call a method using self.name, that attribute may be missing. Inspect the complete starting state each type promises.
class Robot:
def __init__(self, name):
self.name = name
class DeliveryRobot(Robot):
def __init__(self, name, destination):
super().__init__(name)
self.destination = destination
delivery = DeliveryRobot("Courier", "Library")
print(delivery.name, delivery.destination)EXPECTED OUTPUT
Courier Library
04 | OVERRIDE A METHOD
Overriding means the subclass supplies a method with the same name. A call on a delivery instance then uses its describe implementation rather than simply using the base one.
The override can call super().describe() to extend the existing message. Or it can provide a replacement result when that still fits the shared contract. Here, every describe returns text and does not change the object.
Keep parameter expectations and result meaning compatible for supported use. A subclass describe requiring an extra compulsory argument would break a caller expecting describe() with no arguments.
class Robot:
def __init__(self, name):
self.name = name
def describe(self):
return f"Robot {self.name}"
class DeliveryRobot(Robot):
def __init__(self, name, destination):
super().__init__(name)
self.destination = destination
def describe(self):
return super().describe() + f" delivering to {self.destination}"
print(DeliveryRobot("Courier", "Library").describe())EXPECTED OUTPUT
Robot Courier delivering to Library
05 | POLYMORPHISM USES A COMMON REQUEST
Polymorphism lets code use a common operation with objects providing different suitable implementations. A loop calling describe() does not need a separate if branch for every specialised robot.
Python resolves the method using the actual object at runtime. This is dynamic dispatch. A base type reference does not force the base implementation when the object supplies an override.
The benefit is a stable caller: adding another type with a compatible describe method can work without rewriting that reporting loop.
class DeliveryRobot:
def describe(self):
return "Delivering a book"
class SurveyRobot:
def describe(self):
return "Measuring a room"
for robot in [DeliveryRobot(), SurveyRobot()]:
print(robot.describe())EXPECTED OUTPUT
Delivering a book Measuring a room
06 | DUCK TYPING DOES NOT REQUIRE A SHARED BASE
The previous two classes have no custom common base. They still work with the loop because both provide a suitable describe(). This style is often called duck typing: the required behaviour matters more than an explicit inheritance relationship.
Duck typing is not a promise that any object will work. If an object lacks describe, returns an unsuitable value or violates the caller’s assumptions, the interface is incompatible. A type hint or Protocol can later document that expectation.
Inheritance and polymorphism are connected ideas but not synonyms. Inheritance defines a type relationship; polymorphic use can also arise through compatible behaviour without that relationship.
07 | A SUBCLASS MUST REMAIN A SUITABLE SUBSTITUTE
Suppose a base lending item promises checkout() when available. A subclass that always raises an unrelated error may fit the class hierarchy syntactically while failing the behaviour the caller relies on.
Ask whether every supported caller can use the specialised object under the shared contract. Preserve meaningful inputs, result types and state rules. A new implementation should not silently require extra prerequisites that base callers were never told about.
Deep hierarchies can make these obligations harder to track. Prefer a small useful hierarchy and test shared behaviour across each type. Code reuse alone is not enough evidence for an is-a relationship.
08 | TRACE WHICH IMPLEMENTATION IS USED
Choose the actual object type. The fixed caller requests describe() with no arguments, then the model shows the implementation selected and returned text. The caller itself stays the same.
09 | USE INHERITANCE WHEN IT EXPLAINS THE DOMAIN
Inheritance may suit related notification types that all send a message, or specialised game entities that satisfy the same update contract. Composition may suit a robot using a replaceable sensor, because the robot has a sensor rather than being one.
Do not make Library inherit from Book simply to access title. Give Library references to books. Do not force every record into a large base class if the shared behaviour is small or nonexistent.
For an exercise, create two compatible describe methods and one reporting function. Then add an incompatible object deliberately in a controlled test and explain why the common interface matters.
10 | TEST THE SHARED CONTRACT
Test every implementation with the same expected calling pattern. Check that it returns text, requires no extra compulsory arguments and leaves state unchanged when that is part of the contract.
For subclass initialisation, check inherited and new attributes. For an override extending base behaviour, test the combined result rather than merely the subclass’s extra words.
The library project will mainly use composition rather than an inheritance tree. Choosing that design is part of understanding this lesson, not a failure to use the syntax you just learned.
PUT YOUR SKILLS TO WORK
Use the Section 6 challenges to practise these ideas independently. Work in your Python editor, predict results and keep a short test record.
Section 6 challenges Python course navigation