OOP Abstraction · Real‑World Project

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.

📅 Aug 12, 2026 ⏱ 15 min read 🎓 Intermediate 🐍 Python 3.11+
Return to Abstraction Guide

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.

Abstract Media base class 3 concrete media types Interactive playground 4 practice exercises Common pitfalls explained

What You'll Build

Define Media ABC Implement EBook Implement Audiobook Implement Comic Build Library engine Add search functionality Calculate total duration Extend with new media type

Architecture at a Glance

Media (ABC)
├── 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 abstract setUp() and tearDown().

⚠️ 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}"
🔍 Let's examine what just happened
  • Abstract methods (open(), get_description(), get_duration()): These are the contract. Every subclass must implement them. Try instantiating Media() directly and you'll get a TypeError.
  • 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
🎯 Polymorphism in Action

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.

Step 1: ABC + TypeError Step 2: Add Concrete Step 3: Search & Duration Step 4: Challenge — Add Podcast
Step 1 — Try to instantiate Media
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}")
Step 2 — Add concrete classes and list_all()
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}")
Step 3 — Search and total_duration()
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))
Step 4 — Challenge: Add a Podcast class
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), year
  • issue_number and page_count
  • open() should return: "📰 Opening {title}, issue {issue_number}"
  • get_description() should describe the magazine
  • get_duration() should return page_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 a media_type attribute or method to Media.
  • 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_borrowed attribute to Media (set to False by 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?
Download Full Solution (.py) Solutions for all exercises included

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 raise NotImplementedError if not implemented). Always use @abstractmethod to enforce the contract.
  • 2. Forgetting to call super().__init__() — If your ABC has a constructor, subclasses must call super().__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 a Media object, 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.

Abstract Media
Defines the contract for all media types
3 Concrete Types
EBook, Audiobook, Comic — each with unique behavior
Polymorphic Engine
Library works with any Media — no type checking needed
Open/Closed Principle
Open for extension, closed for modification

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.

Back to Abstraction Guide Back to Top

Share This Guide

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.

Weekly Python tutorials AI & ML deep-dives OOP series updates Free, cancel anytime

🔒 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.