Key Takeaways: Inheritance and Polymorphism

One-Sentence Summary

Inheritance lets subclasses reuse and customize parent class behavior, while polymorphism lets you write code that works with any object sharing a common interface — eliminating brittle type-checking and making your code extensible.

The Core Mechanism

Concept What It Does Syntax
Inheritance Subclass gets parent's methods/attributes class Child(Parent):
Method overriding Subclass replaces a parent method Define method with same name
super() Calls the parent's version of a method super().method_name()
Polymorphism Same method call, different behavior per type item.use(player) — each type responds differently
Abstract class Defines contract; can't be instantiated class Foo(ABC): with @abstractmethod
MRO The order Python searches classes for a method ClassName.__mro__

Inheritance vs. Composition

Use Inheritance When... Use Composition When...
The relationship is "is-a" The relationship is "has-a"
Dog is an Animal Car has an Engine
Subclass truly substitutes for parent Object uses another object internally
Hierarchy is 2-3 levels deep max You need flexibility to swap components

The Polymorphism Shift

Before Polymorphism After Polymorphism
if isinstance(x, TypeA): ... x.do_thing()
elif isinstance(x, TypeB): ... Each type defines its own do_thing()
Must update every if/elif for new types New types just work

Common Bugs to Avoid

  1. Forgetting super().__init__() — parent attributes never get set, causing AttributeError later
  2. Using inheritance when composition is appropriate — a Stack is not a list
  3. Deep hierarchies — keep to 2-3 levels; deeper trees are hard to debug
  4. Breaking Liskov — subclasses should work everywhere their parent works (a mutable Square subclass of Rectangle is the classic trap)
  5. Tangled multiple inheritance — limit it to small mixins and check __mro__ when in doubt

Quick Reference: Abstract Base Classes

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        ...  # Subclasses MUST implement this

class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def area(self) -> float:
        return 3.14159 * self.radius ** 2

# Shape() raises TypeError; Circle(2) works because it implements area()

Design Guidelines (Section 15.9)

  1. Favor composition over inheritance
  2. Keep hierarchies shallow (2-3 levels)
  3. Follow Liskov: subclasses must be substitutable for parents
  4. Program to interfaces, not implementations
  5. Use ABCs to enforce contracts in shared codebases

TaskFlow v1.4 Additions

  • DeadlineTask — has due_date, overrides is_overdue() and display()
  • RecurringTask — has frequency, overrides complete() to count the completion and reset, and extends display()
  • ChecklistTask — has a sub-items dict (composition), tracks partial progress, auto-completes when every item is checked

What's Next

Chapter 16: OOP design patterns (Observer, dataclasses, loose coupling) and TaskFlow v1.5 with event notifications.