Digital Library —
Abstract Media,
Unified Reading.
Build a full‑fledged Digital Library using Python abstraction. Learn how abstract base classes (ABCs),
@abstractmethod, and polymorphism enable you to handle eBooks, audiobooks, and comics
through a single, elegant interface.
This project demonstrates the power of abstraction in action — you'll write code that is extensible, maintainable, and ready to support any media format you can imagine.
The Digital Library Challenge
📖 The Problem: A Library That Keeps Growing
Imagine you're building the backend for a digital library app used by thousands of readers. You start with eBooks — straightforward. Then the product team says: "We need audiobooks." A few months later: "Comics are popular — add those too." Then podcasts. Then magazines.
Each new format has unique attributes: pages, duration, narrator, artist, episodes, issues. But they all share common behaviors: you can open them, describe them, and close them.
Abstraction is what saves you from rewriting the entire library engine every time a new format appears. It lets you define what a media item does, while leaving how it does it to each concrete class.
What You'll Build
Architecture at a Glance
├── EBook (pages)
├── Audiobook (duration, narrator)
└── Comic (artist, issues)
Library ── uses ──► Media (any type)
[ Engine depends only on the abstraction ]
The Library engine doesn't know about EBook, Audiobook, or Comic — it only knows Media.
Why Every Programmer Should Learn Abstraction
Without Abstraction: The isinstance Cascade
Here's what the library engine looks like without abstraction — a nightmare of type checking:
❌ Without Abstraction
def display_media(item): if isinstance(item, EBook): print(f"📖 {item.title} by {item.author}") print(f" Pages: {item.pages}") elif isinstance(item, Audiobook): print(f"🎧 {item.title} by {item.author}") print(f" Duration: {item.duration} min") elif isinstance(item, Comic): print(f"🦸 {item.title} by {item.author}") print(f" Artist: {item.artist}") else: raise ValueError("Unknown media type")
✅ With Abstraction
def display_media(item: Media): print(f"{item.get_description()}") print(f" Duration: {item.get_duration()} min")
✗ The left version violates the Open/Closed Principle. Every new media type requires modifying display_media(). The right version works with any Media — no changes needed.
The Liskov Substitution Principle (LSP)
Abstraction naturally enforces the Liskov Substitution Principle — a key SOLID principle that states:
Subtypes must be substitutable for their base types. Any Media subclass can be dropped into the Library without breaking it.
This is exactly what the Library engine relies on — it doesn't care which Media subclass it receives, as long as it implements the abstract interface.
The Template Method Pattern in Action
Notice how close() and display_info() are concrete methods in the Media ABC:
def close(self) -> str: self._is_open = False return f"Closed {type(self).__name__}: {self.title}" def display_info(self) -> str: return f"📚 {self.title} ({self.year}) by {self.author}"
These methods use the abstract title, author, and year attributes that all subclasses provide. This is the Template Method pattern — the base class defines the skeleton of an algorithm, and subclasses fill in the details.
Real-World Analogies
Abstraction is everywhere in Python:
- Django's abstract models — define common fields and methods, then inherit them.
collections.abc— abstract base classes for containers (Sequence, MutableMapping).pathlib.Path— abstract interface for filesystem paths.- Unittest's
TestCase— concrete methods that orchestrate tests, with abstractsetUp()andtearDown().
⚠️ When NOT to use an ABC: Abstraction is a tool, not a rule. If you have only one concrete implementation, an ABC is over-engineering. Wait until you have at least two subclasses that share behavior before abstracting. Premature abstraction adds complexity without benefit.
Defining the Media Contract
Media — The Interface
The Media abstract base class defines the contract that all media types must fulfill. Let's examine each part carefully.
Step 1: Import ABC and abstractmethod
from abc import ABC, abstractmethod
The abc module provides the infrastructure for abstract base classes. ABC is the metaclass that makes a class abstract, and @abstractmethod marks methods that subclasses must implement.
Step 2: Define the Media class
class Media(ABC): def __init__(self, title: str, author: str, year: int): self.title = title self.author = author self.year = year self._is_open = False @abstractmethod def open(self) -> str: """Open the media in its native format.""" pass @abstractmethod def get_description(self) -> str: """Return a human-readable description.""" pass @abstractmethod def get_duration(self) -> int: """Return duration in minutes (0 for non-timed media).""" pass def close(self) -> str: self._is_open = False return f"Closed {type(self).__name__}: {self.title}" def display_info(self) -> str: return f"📚 {self.title} ({self.year}) by {self.author}"
-
Abstract methods (
open(),get_description(),get_duration()): These are the contract. Every subclass must implement them. Try instantiatingMedia()directly and you'll get aTypeError. -
Concrete methods (
close(),display_info()): These provide shared behavior that works for all media types. They're inherited by all subclasses — no duplication needed. -
State tracking (
_is_open): A private attribute that tracks whether the media is currently open. This is shared state management — all subclasses get it for free.
💡 Try this yourself: Try to instantiate Media() directly. You'll see a TypeError saying you can't instantiate an abstract class with abstract methods. This is the ABC enforcing its contract!
Implementing Media Types
EBook — Digital Book
class EBook(Media): def __init__(self, title: str, author: str, year: int, pages: int): super().__init__(title, author, year) self.pages = pages def open(self) -> str: self._is_open = True return f"📖 Opening eBook '{self.title}' — {self.pages} pages loaded." def get_description(self) -> str: return f"📖 {self.title} — {self.pages} pages, digital edition." def get_duration(self) -> int: return 0
Key observation: EBook inherits close() and display_info() from Media — no need to re-implement them.
Audiobook — Audio Format
class Audiobook(Media): def __init__(self, title: str, author: str, year: int, duration_min: int, narrator: str): super().__init__(title, author, year) self.duration_min = duration_min self.narrator = narrator def open(self) -> str: self._is_open = True return f"🎧 Playing audiobook '{self.title}' — narrated by {self.narrator}." def get_description(self) -> str: return f"🎧 {self.title} — {self.duration_min} min, narrated by {self.narrator}." def get_duration(self) -> int: return self.duration_min
Comic — Visual Storytelling
class Comic(Media): def __init__(self, title: str, author: str, year: int, artist: str, issues: int): super().__init__(title, author, year) self.artist = artist self.issues = issues def open(self) -> str: self._is_open = True return f"🦸 Opening comic '{self.title}' — {self.issues} issues by {self.artist}." def get_description(self) -> str: return f"🦸 {self.title} — {self.issues} issues, art by {self.artist}." def get_duration(self) -> int: return self.issues * 5
The same method names (open(), get_description(), get_duration()) produce different results based on the concrete type. The Library engine doesn't care which type it's dealing with — it just calls the methods.
Building the Library Engine
Working with the Abstraction
The Library engine depends only on the Media abstraction. It doesn't know about EBook, Audiobook, or Comic — it simply works with any object that implements the Media interface.
from typing import List class Library: def __init__(self, name: str): self.name = name self._collection: List[Media] = [] def add_media(self, item: Media) -> None: self._collection.append(item) def list_all(self) -> List[str]: return [item.display_info() for item in self._collection] def open_media(self, index: int) -> str: if 0 <= index < len(self._collection): item = self._collection[index] return item.open() raise IndexError("Invalid index") def search(self, query: str) -> List[Media]: query = query.lower() return [item for item in self._collection if query in item.title.lower() or query in item.author.lower()] def total_duration(self) -> int: return sum(item.get_duration() for item in self._collection)
✓ The Library depends only on the Media interface. This is the Open/Closed Principle in action — open for extension (new media types), closed for modification (Library engine remains unchanged).
Putting It All Together
Here's the complete working example with all components:
def main(): lib = Library("Poetic Bytes Digital Library") lib.add_media(EBook("Clean Code", "Robert C. Martin", 2008, 464)) lib.add_media(Audiobook("The Hobbit", "J.R.R. Tolkien", 1937, 600, "Andy Serkis")) lib.add_media(Comic("Watchmen", "Alan Moore", 1986, "Dave Gibbons", 12)) print("📚 Library Collection:") for info in lib.list_all(): print(f" {info}") print("\n▶ Opening item:") print(lib.open_media(1)) print("\n🔍 Search for 'code':") results = lib.search("code") for item in results: print(f" - {item.title}") print(f"\n⏱ Total duration: {lib.total_duration()} minutes")
Explore the Code Step by Step
Run each step to see how the Digital Library system works. Each step builds on the previous one.
from abc import ABC, abstractmethod
class Media(ABC):
def __init__(self, title, author, year):
self.title = title
self.author = author
self.year = year
@abstractmethod
def open(self):
pass
@abstractmethod
def get_description(self):
pass
@abstractmethod
def get_duration(self):
pass
try:
m = Media("Test", "Author", 2024)
except TypeError as e:
print(f"❌ TypeError caught:\n{e}")
from abc import ABC, abstractmethod
from typing import List
class Media(ABC):
def __init__(self, title, author, year):
self.title = title
self.author = author
self.year = year
self._is_open = False
@abstractmethod
def open(self):
pass
@abstractmethod
def get_description(self):
pass
@abstractmethod
def get_duration(self):
pass
def close(self):
self._is_open = False
return f"Closed {type(self).__name__}: {self.title}"
def display_info(self):
return f"📚 {self.title} ({self.year}) by {self.author}"
class EBook(Media):
def __init__(self, title, author, year, pages):
super().__init__(title, author, year)
self.pages = pages
def open(self):
self._is_open = True
return f"📖 Opening eBook '{self.title}' — {self.pages} pages loaded."
def get_description(self):
return f"📖 {self.title} — {self.pages} pages, digital edition."
def get_duration(self):
return 0
class Audiobook(Media):
def __init__(self, title, author, year, duration_min, narrator):
super().__init__(title, author, year)
self.duration_min = duration_min
self.narrator = narrator
def open(self):
self._is_open = True
return f"🎧 Playing audiobook '{self.title}' — narrated by {self.narrator}."
def get_description(self):
return f"🎧 {self.title} — {self.duration_min} min, narrated by {self.narrator}."
def get_duration(self):
return self.duration_min
class Comic(Media):
def __init__(self, title, author, year, artist, issues):
super().__init__(title, author, year)
self.artist = artist
self.issues = issues
def open(self):
self._is_open = True
return f"🦸 Opening comic '{self.title}' — {self.issues} issues by {self.artist}."
def get_description(self):
return f"🦸 {self.title} — {self.issues} issues, art by {self.artist}."
def get_duration(self):
return self.issues * 5
class Library:
def __init__(self, name):
self.name = name
self._collection = []
def add_media(self, item):
self._collection.append(item)
def list_all(self):
return [item.display_info() for item in self._collection]
lib = Library("Poetic Bytes Digital Library")
lib.add_media(EBook("Clean Code", "Robert C. Martin", 2008, 464))
lib.add_media(Audiobook("The Hobbit", "J.R.R. Tolkien", 1937, 600, "Andy Serkis"))
lib.add_media(Comic("Watchmen", "Alan Moore", 1986, "Dave Gibbons", 12))
print("📚 Library Collection:")
for info in lib.list_all():
print(f" {info}")
class Library:
def __init__(self, name):
self.name = name
self._collection = []
def add_media(self, item):
self._collection.append(item)
def list_all(self):
return [item.display_info() for item in self._collection]
def open_media(self, index):
if 0 <= index < len(self._collection):
item = self._collection[index]
return item.open()
raise IndexError("Invalid index")
def search(self, query):
query = query.lower()
return [item for item in self._collection
if query in item.title.lower() or query in item.author.lower()]
def total_duration(self):
return sum(item.get_duration() for item in self._collection)
lib = Library("Poetic Bytes Digital Library")
lib.add_media(EBook("Clean Code", "Robert C. Martin", 2008, 464))
lib.add_media(Audiobook("The Hobbit", "J.R.R. Tolkien", 1937, 600, "Andy Serkis"))
lib.add_media(Comic("Watchmen", "Alan Moore", 1986, "Dave Gibbons", 12))
print("🔍 Search for 'code':")
results = lib.search("code")
for item in results:
print(f" - {item.title}")
print(f"\n⏱ Total duration: {lib.total_duration()} minutes")
print("\n▶ Opening item 1:")
print(lib.open_media(1))
class Podcast(Media):
def __init__(self, title, host, year, episode_count, average_length):
super().__init__(title, host, year)
self.episode_count = episode_count
self.average_length = average_length
def open(self):
self._is_open = True
return f"🎙️ Playing podcast '{self.title}' — episode 1 of {self.episode_count}"
def get_description(self):
return f"🎙️ {self.title} — {self.episode_count} episodes, ~{self.average_length} min each"
def get_duration(self):
return self.episode_count * self.average_length
lib = Library("My Podcast Library")
lib.add_media(Podcast("Tech Talk", "Jane Doe", 2025, 50, 45))
lib.add_media(Podcast("AI Today", "John Smith", 2026, 30, 60))
print("📚 Podcast Collection:")
for info in lib.list_all():
print(f" {info}")
print(f"\n⏱ Total duration: {lib.total_duration()} minutes")
print("\n▶ Opening first podcast:")
print(lib.open_media(0))
✅ The Podcast challenge shows how extensible this design is. You added a new media type without touching the Library engine — that's the power of abstraction!
Reinforce Your Learning
Try these exercises to deepen your understanding of abstraction:
🏋️ Exercise 1: Add a Magazine Class
Implement a Magazine class that extends Media. A magazine has:
title,publisher(use as author),yearissue_numberandpage_countopen()should return: "📰 Opening {title}, issue {issue_number}"get_description()should describe the magazineget_duration()should returnpage_count // 2(estimated reading time in minutes)
Hint: Follow the pattern from the Podcast example above.
🏋️ Exercise 2: Add a get_info() Method
Add a get_info() method to the Media ABC that returns a dict with all metadata.
- Should this be abstract or concrete? Think about it.
- If concrete, what information would every media type have?
- If abstract, what information would each subclass add?
Challenge: Make it concrete and use @property for computed fields.
🏋️ Exercise 3: Filter by Media Type (No isinstance)
Add a filter_by_type() method to Library that returns all media of a given type.
- Without using
isinstance()— add amedia_typeattribute or method toMedia. - Hint: Use an abstract property or a class attribute.
🏋️ Exercise 4: Borrow and Return
Add borrow() and return_item() methods to Library that track whether media is available.
- Add a
_is_borrowedattribute toMedia(set toFalseby default). borrow()should mark it as borrowed and return a message.return_item()should mark it as returned.- What happens if someone tries to borrow an already-borrowed item?
Avoid These Mistakes
5 Common Abstraction Mistakes
- 1. Forgetting
@abstractmethod— Without it, the method is just a normal method that can be called (and will raiseNotImplementedErrorif not implemented). Always use@abstractmethodto enforce the contract. - 2. Forgetting to call
super().__init__()— If your ABC has a constructor, subclasses must callsuper().__init__()to initialize the base class attributes. Otherwise,title,author, etc., won't be set. - 3. Making everything abstract — Not every method needs to be abstract. Concrete methods in the ABC provide shared behavior that subclasses inherit for free.
- 4. Over-abstracting too early — If you only have one concrete implementation, an ABC is over-engineering. Wait until you have at least two subclasses.
- 5. Using
isinstance()checks — If you're checking the type of aMediaobject, you've missed the point. The whole idea is to work with the abstraction, not the concrete type.
💡 Pro Tip: Use @abstractmethod to enforce the contract, but use NotImplementedError only as a fallback. The ABC will catch missing implementations at instantiation time, which is much better than runtime errors.
Summary: Abstraction in Action
🏁 Key Takeaways
You've built a Digital Library using Python's abstraction tools.
This project demonstrates how abstract base classes, @abstractmethod, and
polymorphism work together to create extensible, maintainable code.
Every programmer should learn abstraction because it is the foundation of professional software engineering. The Digital Library project shows you exactly why — you can add new media types without touching the library engine, search works uniformly across all media, and the system remains simple and understandable.
Related Reading
Abstraction is just one pillar of OOP. Explore the others to complete your understanding:
Next step: Inheritance builds on abstraction by allowing you to create hierarchies of classes that share behavior and add new capabilities.
Subscribe to Poetic Bytes
Level up your Python knowledge.
In-depth Python tutorials, AI insights, and software engineering guides — delivered straight to your inbox. No spam, ever.
🔒 Join 8,400+ developers. Your email is never shared or sold.
Follow Poetic Bytes
Stay connected across every platform. Get daily Python tips, OOP insights, and the latest AI developments — wherever you spend your time online.