The Book class from the previous lesson works, but Papyrus doesn't sell only books. Ana wants to add literary magazines (with an issue number) and Pau insists on trying ebooks, which take up no shelf space and have no physical stock. Copying the Book class three times and tweaking it would mean repeating code — and you already know from module 3 that repeated code is code that drifts out of sync. Inheritance solves the problem: a base class (Product) concentrates what's common, and each derived class adds or adjusts only what makes it different.

Contents

  1. The problem: three items, 80% common code
  2. Base class and derived class
  3. super().__init__(): reusing the parent's initialization
  4. Extending vs overriding methods
  5. Papyrus's complete hierarchy
  6. isinstance() and issubclass()
  7. When NOT to inherit: composition as an alternative
  8. The MRO: the order Python searches in
  9. Common mistakes and tips
  10. Exercises with solutions

The problem: three items, 80% common code

Books, magazines and ebooks share almost everything: title, price, the member pricing formula, the stock check. They differ in details: the magazine has an issue number; the ebook has a file format and is always available. Inheritance expresses that "is a" relationship: a Magazine is a Product, an EBook is a Book.

Base class and derived class

First, the base class. We generalize member_price() from 05-01 into a more flexible method, final_price(member=False), which reproduces exactly the signature of the old function from papyrus_utils.py — another piece of module 3 that moves in to live with its data:

class Product:
    """Any item for sale at Papyrus."""

    BOOK_VAT = 0.04
    MEMBER_DISCOUNT = 0.05

    def __init__(self, title, price, stock=0):
        self.title = title
        self.price = price
        self.stock = stock

    def final_price(self, member=False):
        """Price including VAT, with a discount if the customer is a member."""
        discount = Product.MEMBER_DISCOUNT if member else 0
        gross = self.price * (1 - discount) * (1 + Product.BOOK_VAT)
        return round(gross, 2)

    def in_stock(self):
        return self.stock > 0

    def description(self):
        return f"{self.title} — {self.final_price():.2f} EUR"

To derive a class, you write the base class in parentheses:

class Magazine(Product):
    """A literary magazine: like a Product, but with an issue number."""

    def __init__(self, title, issue, price, stock=0):
        super().__init__(title, price, stock)   # delegates the common part to Product
        self.issue = issue                      # and adds its own

Magazine inherits everything it doesn't redefine: final_price(), in_stock(), the class attributes. It already works:

quimera = Magazine("Quimera", 482, 6.50, 10)
print(quimera.final_price(member=True))   # 6.42  → the formula inherited from Product
print(quimera.issue)                      # 482   → its own attribute
print(quimera.in_stock())                 # True  → inherited method, untouched

super().__init__(): reusing the parent's initialization

super() returns access to the base class, and super().__init__(...) runs its initializer. Without that call, title, price and stock would never be created on the Magazine object:

class BrokenMagazine(Product):
    def __init__(self, title, issue, price):
        self.issue = issue   # forgot to call super().__init__!

m = BrokenMagazine("Quimera", 482, 6.50)
m.final_price()   # AttributeError: 'BrokenMagazine' object has no attribute 'price'

The practical rule: if the derived class defines its own __init__, its first line is almost always super().__init__(...) with the arguments the base class needs. If the derived class doesn't define __init__, it inherits the base's automatically and there's nothing to do.

Extending vs overriding methods

A derived class can do three things with each inherited method:

Strategy What it does Example at Papyrus
Inherit as is Doesn't redefine it; uses the parent's Magazine inherits final_price()
Override Redefines it entirely EBook redefines in_stock()
Extend Redefines it, but calls the parent's with super() and adds something Magazine.description() reuses and decorates

Overriding — total replacement:

class EBook(Book):
    def in_stock(self):
        return True   # a digital file never runs out

Extending — the parent does its part and the child completes it:

class Magazine(Product):
    def __init__(self, title, issue, price, stock=0):
        super().__init__(title, price, stock)
        self.issue = issue

    def description(self):
        base = super().description()          # "Quimera — 6.76 EUR"
        return f"[Magazine issue {self.issue}] {base}"

Papyrus's complete hierarchy

Let's assemble the pieces. Book now inherits from Product (and takes the opportunity to gain an author attribute), and EBook inherits from Book, because an ebook is a book:

class Book(Product):
    """A paper book in the catalog."""

    def __init__(self, title, author, price, stock=0):
        super().__init__(title, price, stock)
        self.author = author

    def description(self):
        return f"[Book] {self.title}, by {self.author} — {self.final_price():.2f} EUR"


class Magazine(Product):
    """A numbered literary magazine."""

    def __init__(self, title, issue, price, stock=0):
        super().__init__(title, price, stock)
        self.issue = issue

    def description(self):
        return f"[Magazine issue {self.issue}] {self.title} — {self.final_price():.2f} EUR"


class EBook(Book):
    """A book with no physical copies: delivered by download."""

    def __init__(self, title, author, price, file_format="EPUB"):
        super().__init__(title, author, price)   # stock keeps its default value, 0
        self.file_format = file_format

    def in_stock(self):
        return True   # overridden: always available

    def description(self):
        return f"[Digital {self.file_format}] {self.title}, by {self.author} — {self.final_price():.2f} EUR"
classDiagram
    class Product {
        +BOOK_VAT = 0.04
        +MEMBER_DISCOUNT = 0.05
        +title
        +price
        +stock
        +final_price(member)
        +in_stock()
        +description()
    }
    class Book {
        +author
        +description()
    }
    class Magazine {
        +issue
        +description()
    }
    class EBook {
        +file_format
        +in_stock()
        +description()
    }
    Product <|-- Book
    Product <|-- Magazine
    Book <|-- EBook

In action, with the enriched canonical catalog:

items = [
    Book("The Odyssey", "Homer", 12.50, 4),
    Book("Faust", "Goethe", 21.00, 0),
    Magazine("Quimera", 482, 6.50, 10),
    EBook("Hamlet", "Shakespeare", 4.95, "EPUB"),
]

for item in items:
    status = "available" if item.in_stock() else "OUT OF STOCK"
    print(f"{item.description():<55} [{status}]")
[Book] The Odyssey, by Homer — 13.00 EUR                [available]
[Book] Faust, by Goethe — 21.84 EUR                     [OUT OF STOCK]
[Magazine issue 482] Quimera — 6.76 EUR                 [available]
[Digital EPUB] Hamlet, by Shakespeare — 5.15 EUR        [available]

Notice the key detail: the loop treats all four alike, and each object responds to description() and in_stock() in its own way. That phenomenon has a name — polymorphism — and it's the entire subject of the next lesson.

isinstance() and issubclass()

  • isinstance(obj, SomeClass) asks whether an object is an instance of a class or of any of its derived classes.
  • issubclass(ClassA, ClassB) asks whether one class descends from another.
hamlet_epub = EBook("Hamlet", "Shakespeare", 4.95)

isinstance(hamlet_epub, EBook)      # True
isinstance(hamlet_epub, Book)       # True  — an EBook IS a Book
isinstance(hamlet_epub, Product)    # True  — and a Product too
isinstance(hamlet_epub, Magazine)   # False

issubclass(EBook, Product)          # True
issubclass(Magazine, Book)          # False — they're siblings, not parent and child

Always prefer isinstance(x, Product) over type(x) is Product: the former respects inheritance; the latter only accepts the exact type. Even so, use it sparingly: in 05-03 you'll see that chaining isinstance to decide behavior is a code smell that polymorphism eliminates.

When NOT to inherit: composition as an alternative

Inheritance models "is a". If the real relationship is "has a", the right tool is composition: storing one object inside another as an attribute.

class Shelf:
    """A shelf HAS products; it is not a product."""

    def __init__(self, aisle):
        self.aisle = aisle
        self.products = []   # composition: Product objects inside

    def place(self, product):
        self.products.append(product)

Signs that inheriting is a bad idea:

  • You inherit just to "borrow a couple of methods", but the sentence "X is a Y" sounds forced (is a Shelf a Product? No).
  • The derived class has to cancel out or hollow out half of what it inherits.
  • You only want to reuse code: for that you already have functions, modules (03-04) and composition.

A short rule we'll use for the rest of the course: inheritance for "is a", composition for "has a".

The MRO: the order Python searches in

When you call hamlet_epub.final_price(), Python looks for the method following the MRO (Method Resolution Order): first in EBook, then in Book, then in Product and finally in object, the implicit base class of every Python class. It stops at the first place it finds it — which is why in_stock() answers with the EBook version and final_price() with the Product one.

print(EBook.__mro__)
# (<class 'EBook'>, <class 'Book'>, <class 'Product'>, <class 'object'>)

With single inheritance (one base per class, like the whole Papyrus hierarchy) the MRO is simply the chain of parents. Python also allows multiple inheritanceclass C(A, B) — where the MRO gets subtler; for now it's enough to know it exists and that we won't need it in this course.

Common Mistakes and Tips

  • Forgetting super().__init__(...) in the derived class's __init__: the parent's attributes never get created and the inherited methods fail with AttributeError when used. If you define __init__, delegate first.
  • Swapping the argument order when delegating. super().__init__(price, title, stock) gives no immediate error: it simply stores the price as the title. Use keyword arguments if in doubt: super().__init__(title=title, price=price, stock=stock).
  • Inheriting for convenience, not semantics. If "X is a Y" doesn't hold up when said out loud, use composition. Forced hierarchies cost dearly as they grow.
  • Overriding a method and breaking its contract. If in_stock() returns a bool in Product, the EBook version must return a bool too. Anyone who works with an arbitrary Product relies on that behavior (we'll see it formalized as polymorphism in 05-03).
  • Comparing types with type(x) is Book when you want to include derived classes: an EBook would be left out. isinstance() is almost always what you want.
  • Tip: keep hierarchies shallow (2-3 levels, like Papyrus's). If you need a fourth level, stop and ask yourself whether composition wouldn't solve it better.

Exercises

Exercise 1: the CityMap class

Ana starts selling illustrated maps of literary cities. Create CityMap(Product) with an extra attribute city and a description() that returns "[Map] Joyce's Dublin (Dublin) — 8.32 EUR" (use final_price()). Create one with title "Joyce's Dublin", city "Dublin", price 8.00 and stock 3, and print its description.

Exercise 2: extend, don't override

Add a description() method to EBook that reuses Book's description via super() and appends the suffix " (instant download)", instead of building the whole text from scratch as we did above. Check the result with the digital Hamlet.

Exercise 3: type audit

Given the items list from the lesson, write a snippet that counts how many items are Books (including digital ones) and how many are exactly paper books (that is, Book but not EBook). Use isinstance().

Solutions

Solution 1:

class CityMap(Product):
    """Illustrated map of a literary city."""

    def __init__(self, title, city, price, stock=0):
        super().__init__(title, price, stock)
        self.city = city

    def description(self):
        return f"[Map] {self.title} ({self.city}) — {self.final_price():.2f} EUR"

dublin = CityMap("Joyce's Dublin", "Dublin", 8.00, 3)
print(dublin.description())   # [Map] Joyce's Dublin (Dublin) — 8.32 EUR

CityMap only writes what is its own (city and its description format); price, stock and the VAT formula come free from Product.

Solution 2:

class EBook(Book):
    def __init__(self, title, author, price, file_format="EPUB"):
        super().__init__(title, author, price)
        self.file_format = file_format

    def in_stock(self):
        return True

    def description(self):
        return super().description() + " (instant download)"

hamlet_epub = EBook("Hamlet", "Shakespeare", 4.95)
print(hamlet_epub.description())
# [Book] Hamlet, by Shakespeare — 5.15 EUR (instant download)

This is the "extend" version: if tomorrow Book.description() changes its format, EBook updates itself automatically. It's the same DRY principle that motivated functions back in module 3.

Solution 3:

books = [item for item in items if isinstance(item, Book)]
paper_only = [item for item in items if isinstance(item, Book) and not isinstance(item, EBook)]

print(f"Books (paper + digital): {len(books)}")      # 3
print(f"Paper books only:        {len(paper_only)}") # 2

isinstance(item, Book) also accepts EBooks because they inherit from Book; excluding them has to be asked for explicitly. The list comprehensions from 02-04 are still the cleanest way to filter.

Conclusion

Inheritance has turned the lone class of 05-01 into a family: Product concentrates title, price, stock and the canonical final_price() formula; Book adds an author; Magazine, its issue number; and EBook inherits from Book, contributes the file format and overrides in_stock() because digital goods never run out. You've learned to delegate initialization with super().__init__(), to distinguish inheriting, extending and overriding, to ask about types with isinstance()/issubclass(), to recognize when composition ("has a") beats inheritance ("is a"), and to read the MRO that governs method lookup. But the most interesting observation of the lesson is still pending: that loop which walked through books, magazines and ebooks calling description() without ever asking what each thing was. That power — one message, many answers — is called polymorphism, and it's what makes hierarchies worth building. It's the subject of the next lesson.

Python Programming Course

Module 1: Introduction to Python

Module 2: Control Structures

Module 3: Functions and Modules

Module 4: Data Structures

Module 5: Object-Oriented Programming

Module 6: File Handling

Module 7: Error and Exception Handling

Module 8: Advanced Topics

Module 9: Testing and Debugging

Module 10: Web Development with Python

Module 11: Data Science with Python

Module 12: Final Project

© Copyright 2026. All rights reserved