Object-Oriented Programming~20 min

Inheritance

A subclass automatically gets its parent's behavior and can override specific parts — a specialized "is-a" relationship, not a shortcut for reusing unrelated code.

By the end of this lesson, you can

  • Explain inheritance as a specialized "is-a" relationship, distinct from composition's "has-a"
  • Write a subclass that inherits behavior from a parent class and overrides part of it
  • Recognize when inheritance is misused for code reuse where composition would model the relationship better

Why it matters

A reference-only encyclopedia is still a library book — it has a title, and it’s tracked the same way — except it can never be borrowed. Rather than duplicating LibraryBook entirely just to change one method, inheritance lets a new class automatically get everything LibraryBook already does, and override only the part that’s different.

Mental model

Inheritance models an “is-a” relationship: a ReferenceBook is a LibraryBook — specifically, one that can never be borrowed. That’s different from the previous lesson’s composition, which modeled “has-a” relationships instead. A subclass automatically inherits every attribute and method its parent defines, and can override any of them.

PythonA subclass that overrides one method
class LibraryBook:
    def __init__(self, title):
        self.title = title
        self.is_on_loan = False

    def borrow(self):
        if self.is_on_loan:
            return False
        self.is_on_loan = True
        return True


class ReferenceBook(LibraryBook):
    def borrow(self):
        return False


ref = ReferenceBook("Encyclopedia")
print(ref.title)
print(ref.borrow())
Output
Encyclopedia
False

ReferenceBook never defines __init__ or title itself — it inherits both from LibraryBook unchanged. It only overrides borrow, so ref.title works exactly the way a plain LibraryBook’s would, while ref.borrow() always refuses.

Trace it

Inherited vs. overridden behavior
CallWhere it comes fromResult
ref.titleInherited from LibraryBook.__init__, unchanged"Encyclopedia"
ref.borrow()Overridden by ReferenceBook — LibraryBook's version never runsFalse, always
LibraryBook("Dune").borrow()LibraryBook's own versionTrue (if not already on loan)

Check your understanding

What does this print? class Animal: def speak(self): return "..." class Dog(Animal): def speak(self): return "Woof" class Cat(Animal): pass print(Dog().speak()) print(Cat().speak())
Or reveal the answer without checking

Answer:Woof, then ...
Dog overrides speak(), so Dog().speak() returns 'Woof'. Cat defines no methods at all (pass), so it inherits Animal's speak() unchanged, returning '...'.

A Playlist class needs to reuse a Song class's duration-formatting logic. Is inheriting from Song the right choice?
Or reveal the answer without checking

Answer:No — a playlist isn't a kind of song; it has songs. Composition (Playlist holding Song instances) models this relationship correctly
This is a has-a relationship, not is-a: a Playlist contains Song instances, it isn't a specialized Song. Composition fits; inheriting from Song here would misrepresent the relationship purely to reuse code.

Practice: warm-up

Trace this program — write down what each call returns.

class Shape:
    def area(self):
        return 0

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side * self.side

generic = Shape()
square = Square(4)

Calls: generic.area(), square.area().

Stuck? Reveal one hint at a time.

  1. Hint 1

    Shape defines area() itself — what does it return when nothing overrides it?

  2. Hint 2

    Square overrides area() — trace through its own version using side = 4.

Reveal the trace

Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.

generic.area() -> Shape's own area() runs -> 0
square.area()  -> Square's overridden area() runs -> 4 * 4 = 16

Practice: apply it

A PremiumMember class inherits from Discount purely to reuse its apply(price) method, even though a premium member isn’t a kind of discount:

class Discount:
    def apply(self, price):
        return price * 0.9

class PremiumMember(Discount):
    def __init__(self, name):
        self.name = name
What's the problem with this design, even though PremiumMember().apply(100) works?
Or reveal the answer without checking

Answer:PremiumMember "is a" Discount according to this code, which misrepresents the relationship — a premium member has a discount, or applies one, but isn't one
This reuses apply() at the cost of claiming every PremiumMember is a kind of Discount — a has-a relationship (a member has, or is eligible for, a discount) forced into an is-a shape purely for code reuse.

Modification challenge: rewrite this using composition instead — PremiumMember should hold a Discount instance as an attribute and delegate to its apply method, the way Member composed Account in the previous lesson.

Summary

  • Inheritance models an “is-a” relationship: a subclass automatically inherits its parent’s attributes and methods, and can override any of them.
  • Overriding a method replaces the parent’s version for that subclass only — other subclasses and the parent itself keep their own behavior.
  • Using inheritance purely to reuse code, when the relationship is really “has-a,” misrepresents what the subclass is and exposes more of the parent’s interface than intended.
  • When in doubt, composition is usually the safer default; reach for inheritance specifically when “is a specialized kind of” genuinely describes the relationship.

Key terms