🎵 Media Library Deep Dive

Move beyond syntax to master when and why to use inheritance. This project builds a production-minded media library with abstract base classes, composition, and real-world patterns.

Python 3.11+ ABC + Inheritance Composition Liskov Substitution ~45 min
Back to Inheritance Guide Run the Code

🎯 By the end of this tutorial, you will be able to:

  • Design abstract base classes using abc.ABC and @abstractmethod
  • Apply the Liskov Substitution Principle (LSP) in practice
  • Distinguish between inheritance ("is-a") and composition ("has-a")
  • Build multi-level hierarchies with intermediate abstract classes
  • Use super() correctly in diamond and multi-level inheritance
  • Write polymorphic code that works with any Media subclass
  • Identify anti-patterns and common inheritance pitfalls
  • Understand Method Resolution Order (MRO) and when it matters

📋 Your Learning Progress

Formalizing the Contract with abc.ABC

The NotImplementedError approach works, but Python provides a more formal way: the abc module. Using ABC and @abstractmethod makes the contract explicit and prevents instantiation of incomplete classes.

1

Formal Abstract Base Class

from abc import ABC, abstractmethod class Media(ABC): # Inherits from ABC def __init__(self, title: str, artist: str, duration: int): self.title = title self.artist = artist self._duration = duration # Protected attribute @property def duration(self) -> int: # Getter return self._duration @property def duration_str(self) -> str: # Human-readable minutes, seconds = divmod(self._duration, 60) hours, minutes = divmod(minutes, 60) if hours: return f"{hours}h {minutes}m {seconds}s" elif minutes: return f"{minutes}m {seconds}s" return f"{seconds}s" def is_long(self, threshold: int = 600) -> bool: # Shared behavior return self._duration > threshold @abstractmethod def play(self) -> str: pass # Must be overridden @abstractmethod def info(self) -> str: pass # Must be overridden def __str__(self) -> str: # Nice string representation return self.info()

🔑 Key Benefits of ABC:

  • Explicit contract — subclasses must implement all abstract methods
  • Cannot instantiate the base class directly — prevents "empty" objects
  • Better tooling — IDEs and linters can validate that all abstract methods are implemented
  • Self-documenting — the @abstractmethod decorator makes the intention clear

✅ When to use ABC vs NotImplementedError:

  • Use ABC for formal contracts in library/framework code
  • Use NotImplementedError for lightweight "please override" hints in smaller projects
  • ABC provides compile-time protection (well, runtime in Python, but earlier error)

🧐 Checkpoint: Abstract Methods

What happens if you try to instantiate Media() directly after using @abstractmethod?

You get a TypeError: Can't instantiate abstract class Media with abstract methods play, info. This prevents creating incomplete objects and forces subclasses to implement all required methods.

Intermediate Abstract Classes for Better Organization

Real-world hierarchies often have intermediate levels. Here we add AudioMedia and VideoMedia to group related media types.

🎬 Media (ABC)
title, artist, duration
@abstractmethod play()
@abstractmethod info()
is_long(), duration_str
⬇️ inherits
🎵 AudioMedia
audio_format, bitrate
+ format_info()
🎬 VideoMedia
resolution, framerate
+ video_info()
⬇️ inherits
🎵 Song
album
play(), info()
🎙️ Podcast
episode
play(), info()
📖 Audiobook
narrator, chapters
play(), info()
🎬 Movie
director
play(), info()
📺 TVEpisode
season, episode_num
play(), info()
📌 Abstract classes shown with dashed borders
2

Intermediate Abstract Classes

class AudioMedia(Media): def __init__(self, title, artist, duration, audio_format="mp3", bitrate=128): super().__init__(title, artist, duration) self.audio_format = audio_format self.bitrate = bitrate def format_info(self) -> str: return f"{self.audio_format.upper()} @ {self.bitrate} kbps" class VideoMedia(Media): def __init__(self, title, artist, duration, resolution="1080p", framerate=30): super().__init__(title, artist, duration) self.resolution = resolution self.framerate = framerate def video_info(self) -> str: return f"{self.resolution} @ {self.framerate}fps"

💡 Why use intermediate classes? They group related behavior (e.g., all audio media share format/bitrate) and provide a place for shared methods without duplicating code in each subclass.

3

Concrete Subclasses

class Song(AudioMedia): def __init__(self, title, artist, duration, album="Unknown", **kwargs): super().__init__(title, artist, duration, **kwargs) self.album = album def play(self) -> str: return f"🎵 ♪ Now playing: {self.title} by {self.artist} ({self.duration_str})" def info(self) -> str: return f"🎵 Song: {self.title} | Artist: {self.artist} | Album: {self.album} | {self.duration_str} | {self.format_info()}" class Movie(VideoMedia): def __init__(self, title, artist, duration, director, **kwargs): super().__init__(title, artist, duration, **kwargs) self.director = director def play(self) -> str: return f"🎬 Now playing: {self.title} ({self.duration_str}) — Directed by {self.director}" def info(self) -> str: return f"🎬 Movie: {self.title} | Director: {self.director} | {self.duration_str} | {self.video_info()}"

🧐 Checkpoint: Method Resolution Order

If you call Song.__mro__, what's the order of class lookup?

(<class 'Song'>, <class 'AudioMedia'>, <class 'Media'>, <class 'object'>). Python looks in the class itself, then the parent (AudioMedia), then the grandparent (Media), then object.

"Has-a" vs "Is-a" — The Library Example

A Library is not a Media — it has media items. This is composition, and it's often a better choice than inheritance.

4

Composition: Library Has Media

class Library: def __init__(self, name: str): self.name = name self._items: list[Media] = [] # Composition: Library HAS Media def add(self, item: Media) -> None: self._items.append(item) def play_all(self) -> None: for item in self._items: print(item.play()) def search_by_artist(self, artist: str) -> list[Media]: return [item for item in self._items if item.artist.lower() == artist.lower()] def get_long_media(self, threshold: int = 600) -> list[Media]: return [item for item in self._items if item.is_long(threshold)] def __len__(self) -> int: return len(self._items)

🔑 Inheritance vs Composition — When to Use Each:

Inheritance ("is-a")Composition ("has-a")
"A Song is a Media""A Library has a list of Media"
Use for specialization (subtype relationships)Use for aggregation (containment relationships)
Changes to parent affect childrenChanges to component are isolated
Can lead to deep, brittle hierarchiesMore flexible and easier to test
Prefer when: The subclass truly is a specialized versionPrefer when: The relationship is containment or delegation

🧐 Checkpoint: Composition vs Inheritance

Why is Library using composition instead of inheriting from Media?

A Library is not a type of Media — it has media items. Using inheritance would be semantically wrong ("Library is a Media" doesn't make sense) and would force the Library to implement abstract methods it doesn't need. Composition is the correct choice here.

The Full Media Library Implementation

This production-minded version includes abstract base classes, intermediate classes, type hints, and composition. Click Run to execute it in your browser.

media_library.py
from abc import ABC, abstractmethod
from typing import List, Optional

class Media(ABC):
    def __init__(self, title: str, artist: str, duration: int):
        self.title = title
        self.artist = artist
        self._duration = duration

    @property
    def duration(self) -> int:
        return self._duration

    @property
    def duration_str(self) -> str:
        minutes, seconds = divmod(self._duration, 60)
        hours, minutes = divmod(minutes, 60)
        if hours:
            return f"{hours}h {minutes}m {seconds}s"
        elif minutes:
            return f"{minutes}m {seconds}s"
        return f"{seconds}s"

    def is_long(self, threshold: int = 600) -> bool:
        return self._duration > threshold

    @abstractmethod
    def play(self) -> str:
        pass

    @abstractmethod
    def info(self) -> str:
        pass

    def __str__(self) -> str:
        return self.info()

class AudioMedia(Media):
    def __init__(self, title: str, artist: str, duration: int,
                 audio_format: str = "mp3", bitrate: int = 128):
        super().__init__(title, artist, duration)
        self.audio_format = audio_format
        self.bitrate = bitrate

    def format_info(self) -> str:
        return f"{self.audio_format.upper()} @ {self.bitrate} kbps"

class VideoMedia(Media):
    def __init__(self, title: str, artist: str, duration: int,
                 resolution: str = "1080p", framerate: int = 30):
        super().__init__(title, artist, duration)
        self.resolution = resolution
        self.framerate = framerate

    def video_info(self) -> str:
        return f"{self.resolution} @ {self.framerate}fps"

class Song(AudioMedia):
    def __init__(self, title: str, artist: str, duration: int,
                 album: str = "Unknown", **kwargs):
        super().__init__(title, artist, duration, **kwargs)
        self.album = album

    def play(self) -> str:
        return f"🎵 ♪ Now playing: {self.title} by {self.artist} ({self.duration_str})"

    def info(self) -> str:
        return f"🎵 Song: {self.title} | Artist: {self.artist} | Album: {self.album} | {self.duration_str} | {self.format_info()}"

class Podcast(AudioMedia):
    def __init__(self, title: str, artist: str, duration: int,
                 episode: int = 1, **kwargs):
        super().__init__(title, artist, duration, **kwargs)
        self.episode = episode

    def play(self) -> str:
        return f"🎙️ Now playing: {self.title} — Episode {self.episode} ({self.duration_str})"

    def info(self) -> str:
        return f"🎙️ Podcast: {self.title} | Host: {self.artist} | Episode {self.episode} | {self.duration_str} | {self.format_info()}"

class Movie(VideoMedia):
    def __init__(self, title: str, artist: str, duration: int,
                 director: str, **kwargs):
        super().__init__(title, artist, duration, **kwargs)
        self.director = director

    def play(self) -> str:
        return f"🎬 Now playing: {self.title} ({self.duration_str}) — Directed by {self.director}"

    def info(self) -> str:
        return f"🎬 Movie: {self.title} | Director: {self.director} | {self.duration_str} | {self.video_info()}"

class Library:
    def __init__(self, name: str):
        self.name = name
        self._items: List[Media] = []

    def add(self, item: Media) -> None:
        self._items.append(item)

    def play_all(self) -> None:
        for item in self._items:
            print(item.play())

    def search_by_artist(self, artist: str) -> List[Media]:
        return [item for item in self._items if item.artist.lower() == artist.lower()]

    def get_long_media(self, threshold: int = 600) -> List[Media]:
        return [item for item in self._items if item.is_long(threshold)]

    def __len__(self) -> int:
        return len(self._items)

    def display_report(self) -> None:
        print(f"\n📚 {self.name} Library Report")
        print(f"Total items: {len(self)}")
        for item in self._items:
            print(f"  - {item.info()}")

# --- Demonstration ---
def demonstrate_polymorphism(items: List[Media]) -> None:
    print("\n🎯 Polymorphism in action:")
    for item in items:
        print(item.play())
        print(item.info())
        print("-" * 40)

# Build the library
library = Library("Poetic Bytes")

# Add various media
library.add(Song("Bohemian Rhapsody", "Queen", 354, "A Night at the Opera", bitrate=320))
library.add(Song("Shape of You", "Ed Sheeran", 233))
library.add(Podcast("Tech Talk", "Sarah Johnson", 2400, episode=42, audio_format="aac"))
library.add(Movie("Inception", "Christopher Nolan", 8880, "Christopher Nolan", resolution="4K"))
library.add(Movie("The Matrix", "Wachowski Sisters", 8160, "Lana & Lilly Wachowski"))

# Display the library report
library.display_report()

# Show polymorphism
demonstrate_polymorphism(library._items[:3])

# Demonstrate search and filtering
print("\n🔍 Search results for 'Queen':")
for item in library.search_by_artist("Queen"):
    print(f"  - {item.info()}")

print("\n⏱️ Long media (> 10 minutes):")
for item in library.get_long_media(600):
    print(f"  - {item.info()}")

# Demonstrate MRO
print("\n📐 Method Resolution Order for Song:")
print([cls.__name__ for cls in Song.__mro__])

💡 What makes this "production-minded"? Type hints, property getters, composition (Library), search/filter capabilities, MRO demonstration, and a clean separation of concerns. This is closer to how real media libraries are structured.

What You'll See

The program builds a library, displays a report, demonstrates polymorphism, and shows search/filter capabilities.

📚 Poetic Bytes Library Report Total items: 5 - 🎵 Song: Bohemian Rhapsody | Artist: Queen | Album: A Night at the Opera | 5m 54s | MP3 @ 320 kbps - 🎵 Song: Shape of You | Artist: Ed Sheeran | Album: Unknown | 3m 53s | MP3 @ 128 kbps - 🎙️ Podcast: Tech Talk | Host: Sarah Johnson | Episode 42 | 40m 0s | AAC @ 128 kbps - 🎬 Movie: Inception | Director: Christopher Nolan | 2h 28m 0s | 4K @ 30fps - 🎬 Movie: The Matrix | Director: Lana & Lilly Wachowski | 2h 16m 0s | 1080p @ 30fps 🎯 Polymorphism in action: 🎵 ♪ Now playing: Bohemian Rhapsody by Queen (5m 54s) 🎵 Song: Bohemian Rhapsody | Artist: Queen | Album: A Night at the Opera | 5m 54s | MP3 @ 320 kbps ---------------------------------------- 🎵 ♪ Now playing: Shape of You by Ed Sheeran (3m 53s) 🎵 Song: Shape of You | Artist: Ed Sheeran | Album: Unknown | 3m 53s | MP3 @ 128 kbps ---------------------------------------- 🎙️ Now playing: Tech Talk — Episode 42 (40m 0s) 🎙️ Podcast: Tech Talk | Host: Sarah Johnson | Episode 42 | 40m 0s | AAC @ 128 kbps ---------------------------------------- 🔍 Search results for 'Queen': - 🎵 Song: Bohemian Rhapsody | Artist: Queen | Album: A Night at the Opera | 5m 54s | MP3 @ 320 kbps ⏱️ Long media (> 10 minutes): - 🎙️ Podcast: Tech Talk | Host: Sarah Johnson | Episode 42 | 40m 0s | AAC @ 128 kbps - 🎬 Movie: Inception | Director: Christopher Nolan | 2h 28m 0s | 4K @ 30fps - 🎬 Movie: The Matrix | Director: Lana & Lilly Wachowski | 2h 16m 0s | 1080p @ 30fps 📐 Method Resolution Order for Song: ['Song', 'AudioMedia', 'Media', 'ABC', 'object']

🔍 Key takeaways from the output:

  • Human-readable durations — the duration_str property converts seconds to a readable format
  • Polymorphism — the same play() and info() calls work on all media types
  • Composition — Library manages a collection of Media objects
  • MRO — shows the full inheritance chain: Song → AudioMedia → Media → ABC → object

The Liskov Substitution Principle & Design Trade-offs

1. Liskov Substitution Principle (LSP)

"Subtypes must be substitutable for their base types." In practice: any subclass of Media should work anywhere a Media is expected.

❌ Anti-pattern — Breaking LSP:

class BrokenMedia(Media): def play(self) -> str: raise RuntimeError("This media cannot be played") # ❌ Violates LSP

If a subclass throws an exception where the base class would work, it breaks LSP. Code that expects Media would fail unexpectedly.

✅ Following LSP:

class GoodMedia(Media): def play(self) -> str: return f"▶️ Playing {self.title}" # ✅ Works like any Media
2. When Inheritance Becomes a Liability

Deep inheritance trees (5+ levels) become hard to maintain. Consider these warning signs:

  • Fragile base class — changes to the base class break many subclasses
  • Deep hierarchies — hard to understand the full behavior chain
  • God objects — base class accumulates too much functionality
  • Subclass explosion — many subclasses needed for small variations

In modern Python, many teams prefer composition + protocols over deep inheritance.

3. Real-World Patterns: How Spotify/Netflix Actually Work

Large-scale systems often use composition over inheritance:

  • Content models are typically flat with type fields, not deep hierarchies
  • Interfaces/Protocols define contracts without inheritance
  • Mixins provide shared behavior without deep trees
  • Dependency injection for flexibility

The hierarchy we built is a great learning tool, but real production code often uses typing.Protocol or abc.ABC with shallow hierarchies.

Common Anti-Patterns & How to Fix Them

Anti-PatternWhy It's BadBetter Approach
Forgetting super().__init__()Parent attributes are never initializedAlways call super().__init__() first
Deep inheritance (5+ levels)Hard to maintain, fragilePrefer shallow hierarchies + composition
Base class knows about subclassesViolates Open/Closed PrincipleKeep base class generic
Overriding to change a tiny detailAdds unnecessary complexityUse parameters or composition
Mutable defaults in __init__Shared state across instancesUse None and create fresh objects
Subclass violates LSPBreaks polymorphic codeEnsure subclasses fulfill the contract

📐 Method Resolution Order (MRO) Deep Dive:

Python resolves method calls using the C3 linearization algorithm. You can always check the order with ClassName.__mro__. In our hierarchy:

Song.__mro__ → (Song, AudioMedia, Media, ABC, object)

This order matters when you have multiple inheritance or when parent classes define the same method. Understanding MRO helps you debug tricky inheritance issues.

Reinforce Your Learning

🧪 Exercise 1: Add an Audiobook with Chapters

Create an Audiobook class that inherits from AudioMedia. It should have:

  • A narrator attribute
  • A chapters list (list of chapter titles)
  • A play() method that includes the narrator
  • An info() method that shows chapter count
  • A get_chapter(chapter_num) method that returns the chapter title

Your Audiobook should inherit from AudioMedia, call super().__init__(), add narrator and chapters, implement play(), info(), and get_chapter(). Test it with a book that has multiple chapters.

🧪 Exercise 2: Fix the LSP Violation

This class violates the Liskov Substitution Principle. Find and fix the issue:

The play() method raises an exception based on rating. This violates LSP because a BrokenMovie cannot be used where a Movie is expected. Fix: remove the exception or make it a warning. Alternatively, add a can_play() method that checks the rating without throwing.

🧪 Exercise 3: Predict the Output

What will this code print? Think about MRO and method lookup.

class A: def whoami(self): return "A" class B(A): def whoami(self): return "B" + super().whoami() class C(A): def whoami(self): return "C" + super().whoami() class D(B, C): def whoami(self): return "D" + super().whoami() print(D().whoami()) print([cls.__name__ for cls in D.__mro__])
DCBA
['D', 'B', 'C', 'A', 'object']
                        

This demonstrates the diamond problem and how MRO resolves it. Python uses C3 linearization to determine the method resolution order: D → B → C → A → object.

🧪 Exercise 4: Implement Pause/Resume Polymorphically

Add pause() and resume() methods to the Media class that work for all media types. Each media type should show its own pause/resume message.

Add @abstractmethod for pause() and resume() in the base Media class. Each subclass implements them with its own emoji and message. This demonstrates that polymorphism works for any method, not just play().

📝 Test Your Understanding

Question 1: What does @abstractmethod do?

Question 2: What is the Liskov Substitution Principle?

Question 3: When should you prefer composition over inheritance?

Question 4: What is Method Resolution Order (MRO)?

Question 5: Which is an anti-pattern in inheritance?

Question 6: What's the benefit of using ABC vs NotImplementedError?

Where to Go From Here

📐
Try Other Inheritance Projects

Zoo Manager, Vehicle Garage, RPG Characters — each applies inheritance in a different domain.

🔍
Learn About Protocols

typing.Protocol provides duck typing without inheritance — a powerful alternative.

🧩
Explore Mixins

Mixins let you compose behavior without deep inheritance. A natural next step after mastering single inheritance.

🧪
Write Tests

Practice writing unit tests for your media classes using pytest. Test both the interface and the polymorphic behavior.

Back to Inheritance Guide