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
- The problem: three items, 80% common code
- Base class and derived class
super().__init__(): reusing the parent's initialization- Extending vs overriding methods
- Papyrus's complete hierarchy
isinstance()andissubclass()- When NOT to inherit: composition as an alternative
- The MRO: the order Python searches in
- Common mistakes and tips
- 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 ownMagazine 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, untouchedsuper().__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:
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 childAlways 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
ShelfaProduct? 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.
With single inheritance (one base per class, like the whole Papyrus hierarchy) the MRO is simply the chain of parents. Python also allows multiple inheritance — class 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 withAttributeErrorwhen 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 aboolinProduct, theEBookversion must return abooltoo. Anyone who works with an arbitraryProductrelies on that behavior (we'll see it formalized as polymorphism in 05-03). - Comparing types with
type(x) is Bookwhen you want to include derived classes: anEBookwould 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 EURCityMap 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)}") # 2isinstance(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
- Introduction to Python
- Setting Up the Development Environment
- Python Syntax and Basic Data Types
- Variables and Constants
- Basic Input and Output
- Virtual Environments and Package Management
Module 2: Control Structures
Module 3: Functions and Modules
- Defining Functions
- Function Arguments
- Lambda Functions
- Modules and Packages
- Standard Library Overview
Module 4: Data Structures
Module 5: Object-Oriented Programming
Module 6: File Handling
Module 7: Error and Exception Handling
- Introduction to Exceptions
- Handling Exceptions
- Raising Exceptions
- Custom Exceptions
- Best Practices and Error Logging
Module 8: Advanced Topics
- Type Hints
- Decorators
- Generators
- Context Managers
- Concurrency: Threads and Processes
- Asyncio for Asynchronous Programming
Module 9: Testing and Debugging
- Introduction to Testing
- Unit Testing with unittest
- Testing with pytest
- Test-Driven Development
- Debugging Techniques
- Using pdb for Debugging
Module 10: Web Development with Python
- Introduction to Web Development
- Flask Framework Fundamentals
- Building REST APIs with Flask
- Introduction to Django
- Building Web Applications with Django
Module 11: Data Science with Python
- Introduction to Data Science
- NumPy for Numerical Computing
- Pandas for Data Manipulation
- Matplotlib for Data Visualization
- Introduction to Machine Learning with scikit-learn
