Polymorphism and Complete OOP Application
advanced40 minLearning objectives
- Explain polymorphism conceptually
- Develop a complete object-oriented application using multiple classes
Learn
AQA 4.1.2.3 — Polymorphism, composition, and a complete application
Retrieval: the previous lesson gave Book, EBook and AudioBook each their own version of borrow(). This lesson names what you already built — polymorphism — and adds the final piece needed for a complete system: a Library class that contains books, rather than being one.
Key vocabulary
- Polymorphism — the same method call behaving differently depending on which object's class actually defines it.
- Composition — building a class out of other objects it contains and manages, rather than by inheriting from them.
- "is-a" vs "has-a" — inheritance models an is-a relationship (an
EBookis aBook); composition models a has-a relationship (aLibraryhas a collection ofBooks — aLibraryis not itself a kind of book at all).
Understand — polymorphism is what you already built
items = [
Book("The Hobbit", "Tolkien", "1"),
EBook("Quiet Life", "Author", "2", 2.4),
AudioBook("Circe", "Miller", "3", "Perdita Weeks"),
]
for item in items:
print(item.describe())
The loop calls .describe() identically on every item, never checking what type each one actually is — yet each one runs its own version, because Python looks up describe starting from the object's own class first, exactly as traced in the previous lesson. This is polymorphism: one method call, several genuinely different behaviours, selected automatically by which object it's called on.
Understand — composition, a genuinely different relationship
Inheritance answers "is this new class a specific kind of an existing one?" (EBook is a Book, just one that borrows differently). A Library is not a kind of book at all — it has books. Modelling that with inheritance (class Library(Book):) would be a genuine design mistake: a library doesn't have a title or an author of its own, and "borrowing a library" makes no sense. Composition — the Library simply containing a list of Book objects as one of its own attributes — is the correct relationship here.
See it — the Library class
class Library:
def __init__(self, name):
self.name = name
self.books = [] # composition: Library HAS a list of Book objects
def add_book(self, book):
self.books.append(book)
def find_available(self):
return [book for book in self.books if getattr(book, "is_available", True)]
def describe_all(self):
for book in self.books:
print(book.describe())
describe_all doesn't care whether each item in self.books is a Book, an EBook or an AudioBook — it just calls .describe() on each, and polymorphism handles the rest.
Debug it — diagnose, explain, fix, test, justify (object-state bug)
class Library:
def __init__(self, name, books=[]):
self.name = name
self.books = books
def add_book(self, book):
self.books.append(book)
central = Library("Central")
central.add_book(Book("The Hobbit", "Tolkien", "1"))
branch = Library("Branch")
print(len(branch.books))
This prints 1, not 0 — the brand-new branch library somehow already contains central's book, despite never having add_book called on it at all.
- Diagnose: does every call to
Library(...)genuinely create its own separate, empty list, or could they be sharing one? - Explain: a default parameter value like
books=[]is created once, when the function is defined — not fresh, every single time the function is called. What does that mean for everyLibraryobject that doesn't explicitly supply its ownbooksargument? - Fix: change the constructor so every
Librarygenuinely gets its own independent, empty list. - Test: confirm a fresh
Librarynow starts with0books, regardless of how many otherLibraryobjects already exist. - Justify: explain why this bug is specifically about mutable default arguments (a list), and wouldn't happen with an immutable default like
books=Noneused correctly.
(Every Library object that doesn't pass its own books argument is silently sharing the exact same list object, created once when init was defined - a well-known Python trap. The fix is to use a sentinel: def init(self, name, books=None): self.books = books if books is not None else []; this creates a genuinely new empty list on every call that needs one. Immutable defaults (like None) don't cause this because they can never be appended-to or mutated in place - a new value has to be explicitly reassigned, which never leaks between objects.)
Debug it — diagnose, explain, fix, test, justify (composition/interaction error)
A colleague later adds a Magazine class to the same system — a genuinely different kind of item, built independently rather than inheriting from Book at all, with its own describe() method but no is_available attribute:
class Magazine:
def __init__(self, title, issue_number):
self.title = title
self.issue_number = issue_number
def describe(self):
return f"{self.title}, issue {self.issue_number}"
class Library:
def __init__(self, name):
self.name = name
self.books = []
def add_book(self, book):
self.books.append(book)
def find_available(self):
return [book for book in self.books if book.is_available]
central = Library("Central")
central.add_book(Book("The Hobbit", "Tolkien", "1"))
central.add_book(Magazine("Wired", 412))
print(central.find_available())
This crashes with AttributeError: 'Magazine' object has no attribute 'is_available'.
- Diagnose:
find_availableassumes every item inself.bookshas anis_availableattribute it can safely read directly — is that assumption actually true for every possible item theLibrarymight contain? - Explain: why does a method written correctly while only
Book-family objects existed break the moment the collection also contains something built completely independently? - Fix: rewrite
find_availableso it works correctly regardless of whether an item tracksis_availableat all (getattr(book, "is_available", True), treating anything without the attribute as always available, is one valid approach). - Test: confirm
find_availablenow runs without crashing on a mixed collection, and returns sensible results for every kind of item. - Justify: explain why this is a genuine composition/interaction bug, not an inheritance bug like the ones in the previous lesson.
(find_available assumes a uniform interface across every object Library holds, but composition specifically allows genuinely unrelated classes to be mixed together in the same collection - Magazine was never designed to satisfy Book's assumptions, and nothing about composition requires that it should be. The fix is to handle the missing attribute gracefully, e.g. via getattr(book, "is_available", True). This is a composition/interaction bug, not an inheritance bug, because Magazine isn't a broken subclass of Book at all - it's an entirely separate class that Library's own method wrongly assumed every contained item would resemble.)
Common mistake
Reaching for inheritance when composition is the correct relationship (or vice versa) — asking "is a Library a kind of Book?" (no — composition) versus "is an EBook a kind of Book?" (yes — inheritance) is exactly the right test. Using inheritance where a class merely contains or uses another, unrelated object leads to exactly the nonsensical "borrowing a library" problem described above.
Evaluate
For the library system built across this sequence, evaluate whether an object-oriented approach was genuinely justified compared with the procedural version from earlier in this sequence — specifically: which of OOP's benefits (bundled data+behaviour, encapsulation, inheritance avoiding duplication, polymorphism handling mixed types uniformly) were actually used, and would the procedural version have needed to solve the same problems some other way?
Justify
A colleague suggests Library should inherit from Book instead of containing a list of them, arguing "it would save writing a separate class." Justify why this is a poor design decision, referring to the is-a/has-a distinction explicitly.
Apply it — develop the complete application
Bring everything from this sequence together: create a Library containing a mix of Book, EBook and AudioBook objects, plus at least one Member (from the previous lesson's Design-it task) who borrows from it. Your program should demonstrate: object creation, at least one encapsulated attribute, inheritance with a genuine override, and a polymorphic loop that treats every kind of book uniformly.
Challenge
Extend the complete application so Library has a method most_borrowed() that returns whichever Book (or subclass) object has the highest times_borrowed() count across the whole collection.
Why this matters for the NEA
A working system combining several interacting classes — some related by inheritance, some by composition, with encapsulated internal state and methods that behave correctly across a mix of object types — is precisely the shape of a genuine NEA project's own architecture, not a simplified classroom version of it. The specific library system here is illustrative; the design reasoning (is-a vs has-a, when to override, when to encapsulate) is the transferable skill.
Looking ahead: Sequence 12 (Advanced Data Structures) returns to graphs, trees and hash tables — data structures you'll now be able to recognise are themselves often naturally modelled as classes, exactly like the ones built across this sequence.