AmouAI Hub/Courses/Programming Fundamentals/Day 14
Inheritance & Polymorphism
One call, different behaviour. Build the hierarchy in Python first — Week 4 rebuilds the same design in Java and the contrast does the teaching.
By the end of today you can
- Write a subclass and override a method
- Call the parent's version with
super(), and say why you usually should - Trace attribute lookup up the inheritance chain
- Explain polymorphism without using the word "polymorphism"
- Say what duck typing is, and what it costs
- Use an abstract base class to make a contract explicit
- Recognise a hierarchy that has gone too deep
▶Today's videos
Watch each video, then work the matching sections below. Watching alone will not do it.
super() and the bug when you forget it · attribute lookup walking the chain · a describe() defined once that works for every subclass.1Overriding and super()
Inherit everything, then change the part that differs.
class Shape:
"""A shape with a name. Area is undefined here."""
def __init__(self, name):
self.name = name
def area(self):
raise NotImplementedError("subclasses must define area")
def describe(self):
return f"{self.name} with area {self.area():.2f}"
class Circle(Shape):
def __init__(self, radius):
super().__init__("circle") # let Shape do its part
self.radius = radius
def area(self): # override
return 3.14159 * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width, height):
super().__init__("rectangle")
self.width = width
self.height = height
def area(self):
return self.width * self.height
print(Circle(2).describe())
print(Rectangle(3, 4).describe())Leave it out and self.name never gets set, so describe() fails with AttributeError: 'Circle' object has no attribute 'name' — and the traceback points at describe, not at the constructor where the mistake actually is.
Look closely at describe(). It is defined once, on Shape, and it calls self.area() — which does not exist on Shape in any useful form. Yet it works for every subclass. That is the whole idea, and section 2 names it.
Attribute lookup walks up the chain and stops at the first match. Square defines no area, so Python finds Rectangle's. Nothing defines describe except Shape, so every object in the tree shares that one implementation.
2Polymorphism
One call, different behaviour — and no if-statement anywhere.
shapes = [Circle(1), Rectangle(2, 3), Circle(0.5)]
for shape in shapes:
print(shape.describe())
total = sum(s.area() for s in shapes)
print(f"total area {total:.2f}")The loop does not know or care what kind of shape it has. It calls describe() and the object decides what that means. Adding a Triangle class requires zero changes to this loop — and that is the entire practical payoff of inheritance.
Duck typing
If it walks like a duck and quacks like a duck, treat it as a duck. Python never checks the type — it just calls the method and sees what happens.
class Invoice:
"""Not a Shape. Does not inherit from anything."""
def describe(self):
return "invoice #4471"
things = [Circle(1), Rectangle(2, 3), Invoice()]
for t in things:
print(t.describe())| Duck typing gives you | And costs you |
|---|---|
| Flexibility — no shared base class needed | No compile-time guarantee the method exists |
| Easy fakes and test doubles | AttributeError at runtime, possibly in production |
| Less ceremony | The contract is implicit — nothing states it |
Java will not let you do this. It demands the contract be written down as an interface before the code compiles. Which is better depends entirely on the project — and having felt both is the reason this course teaches two languages.
3Abstract base classes
Writing the contract down, in Python.
raise NotImplementedError in the base class works, but it fails late — only when someone actually calls the missing method. An abstract base class fails at the moment you try to create the object.
from abc import ABC, abstractmethod
class Shape(ABC):
def __init__(self, name):
self.name = name
@abstractmethod
def area(self):
"""Every shape must define this."""
def describe(self):
return f"{self.name} with area {self.area():.2f}"
class Blob(Shape):
def __init__(self):
super().__init__("blob")
# area() deliberately missing
b = Blob()On 3.9-3.11 you get the message above. On 3.12 and later it reads Can't instantiate abstract class Blob without an implementation for abstract method 'area'. Same error, same cause — do not be thrown if your version phrases it differently.
The error arrives at construction, names the class and names the missing method. Compare that with an AttributeError deep inside a report-generation function three weeks later.
| Approach | Fails when | Use when |
|---|---|---|
| Nothing — pure duck typing | The method is called | Small scripts, quick work |
raise NotImplementedError | The method is called | You want a base class but not the import |
ABC + @abstractmethod | The object is created | The contract matters to other people |
How deep should a hierarchy go?
Every level adds a place to look when you are trying to work out where a method actually comes from. If you find yourself writing Animal → Mammal → Canine → Dog → WorkingDog, ask what behaviour genuinely differs at each level — and whether a held object would say it more clearly than a parent class.
Today's work: build a small shape hierarchy with an ABC. Keep the file — on Day 19 you will rebuild this exact design in Java, and the differences are the lesson.
>_Python playground
A real Python interpreter running inside your browser. Nothing is installed, nothing is uploaded, nothing can break.
input(), comma separated →
✓Exercise set
Checked automatically the moment you submit. Work top to bottom — each one assumes the last. Your answers are saved in this browser.
Day 15 — Exceptions, Testing & Mini-Project 3
Failing well, and proving you did. Plus Mini-Project 3.