🎵 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.
🎯 By the end of this tutorial, you will be able to:
- Design abstract base classes using
abc.ABCand@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
Mediasubclass - 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.
Formal Abstract Base Class
🔑 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
@abstractmethoddecorator makes the intention clear
✅ When to use ABC vs NotImplementedError:
- Use
ABCfor formal contracts in library/framework code - Use
NotImplementedErrorfor lightweight "please override" hints in smaller projects ABCprovides 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?
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.
Intermediate Abstract Classes
💡 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.
Concrete Subclasses
🧐 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.
Composition: Library Has Media
🔑 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 children | Changes to component are isolated |
| Can lead to deep, brittle hierarchies | More flexible and easier to test |
| Prefer when: The subclass truly is a specialized version | Prefer when: The relationship is containment or delegation |
🧐 Checkpoint: Composition vs Inheritance
Why is Library using composition instead of inheriting from Media?
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.
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.
🔍 Key takeaways from the output:
- Human-readable durations — the
duration_strproperty converts seconds to a readable format - Polymorphism — the same
play()andinfo()calls work on all media types - Composition —
Librarymanages a collection ofMediaobjects - MRO — shows the full inheritance chain: Song → AudioMedia → Media → ABC → object
The Liskov Substitution Principle & Design Trade-offs
"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:
If a subclass throws an exception where the base class would work, it breaks LSP. Code that expects Media would fail unexpectedly.
✅ Following LSP:
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.
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-Pattern | Why It's Bad | Better Approach |
|---|---|---|
Forgetting super().__init__() | Parent attributes are never initialized | Always call super().__init__() first |
| Deep inheritance (5+ levels) | Hard to maintain, fragile | Prefer shallow hierarchies + composition |
| Base class knows about subclasses | Violates Open/Closed Principle | Keep base class generic |
| Overriding to change a tiny detail | Adds unnecessary complexity | Use parameters or composition |
| Mutable defaults in __init__ | Shared state across instances | Use None and create fresh objects |
| Subclass violates LSP | Breaks polymorphic code | Ensure 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:
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
narratorattribute - A
chapterslist (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.
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.