Inheritance and Polymorphism
Inheritance clicked for me when I was building a content management system and realized I was copying the same methods across different content types. Articles, videos, and images all needed titles, creation dates, and publishing logic, but each had unique behaviors too. That’s when inheritance transformed from an abstract concept into a practical tool for eliminating code duplication while preserving flexibility.
Python’s inheritance model is remarkably flexible compared to languages like Java or C++. You can inherit from multiple classes, override methods selectively, and even modify the inheritance chain at runtime. This flexibility is powerful, but it also means you need to understand the underlying mechanisms to avoid common pitfalls.
Building Your First Inheritance Hierarchy
Let’s start with a practical example that demonstrates the core concepts. I’ll use a media library system because it naturally illustrates how different types of objects can share common behavior while maintaining their unique characteristics:
class MediaItem:
def __init__(self, title, creator, year):
self.title = title
self.creator = creator
self.year = year
self.views = 0
def play(self):
self.views += 1
return f"Playing {self.title}"
def get_info(self):
return f"{self.title} by {self.creator} ({self.year})"
class Movie(MediaItem):
def __init__(self, title, director, year, duration):
super().__init__(title, director, year)
self.duration = duration
def play(self):
result = super().play() # Call parent method
return f"{result} - Duration: {self.duration} minutes"
The Movie
class inherits all the functionality from MediaItem
but adds movie-specific features like duration tracking. This demonstrates the power of inheritance—you can reuse common functionality while extending it for specific needs. The super()
function lets us call the parent class’s methods, which is crucial for extending behavior rather than completely replacing it. This approach eliminates code duplication while maintaining the ability to customize behavior for different types of media.
Understanding Method Resolution Order
Python uses a specific algorithm called Method Resolution Order (MRO) to determine which method to call when you have complex inheritance hierarchies. This becomes important when you’re dealing with multiple inheritance:
class Playable:
def play(self):
return "Generic playback started"
class Downloadable:
def play(self):
return "Playing downloaded content"
class StreamingVideo(Playable, Downloadable):
def __init__(self, title, url):
self.title = title
self.url = url
video = StreamingVideo("Python Tutorial", "https://example.com")
print(video.play()) # "Generic playback started"
The MRO determines that Playable.play()
gets called because Playable
appears first in the inheritance list. Understanding MRO helps you predict and control method resolution in complex hierarchies. Python uses the C3 linearization algorithm to create a consistent method resolution order that respects the inheritance hierarchy while avoiding ambiguity.
Polymorphism in Action
Polymorphism is where object-oriented programming really shines. The ability to treat different types of objects uniformly, while still getting type-specific behavior, makes your code incredibly flexible. Here’s how it works in practice:
class AudioBook(MediaItem):
def __init__(self, title, narrator, year):
super().__init__(title, narrator, year)
self.current_chapter = 1
def play(self):
super().play()
return f"Playing chapter {self.current_chapter} of {self.title}"
class Podcast(MediaItem):
def __init__(self, title, host, year, episode_number):
super().__init__(title, host, year)
self.episode_number = episode_number
def play(self):
super().play()
return f"Playing episode {self.episode_number}: {self.title}"
# Polymorphism allows uniform treatment
media_library = [
Movie("The Matrix", "Wachowski Sisters", 1999, 136),
AudioBook("Dune", "Scott Brick", 2020),
Podcast("Python Bytes", "Michael Kennedy", 2023, 350)
]
for item in media_library:
print(item.play()) # Each type implements play() differently
This polymorphic behavior lets you write code that works with any type of media item without knowing the specific type at compile time. You can add new media types later without changing the existing code that processes the library. This is the essence of the open/closed principle—your code is open for extension but closed for modification.
Advanced Method Overriding Techniques
Sometimes you need more control over method overriding than simple replacement. Python provides several techniques for sophisticated method customization:
class SecureMediaItem(MediaItem):
def __init__(self, title, creator, year, access_level="public"):
super().__init__(title, creator, year)
self.access_level = access_level
self._access_log = []
def play(self):
# Add security check before calling parent method
if not self._check_access():
return "Access denied"
# Log the access attempt
from datetime import datetime
self._access_log.append(datetime.now())
# Call parent method and modify result
result = super().play()
return f"[SECURE] {result}"
def _check_access(self):
# Simplified access control
return self.access_level == "public"
def get_access_history(self):
return f"Accessed {len(self._access_log)} times"
This pattern of calling the parent method and then modifying its behavior is incredibly common in real-world applications. You’re extending functionality rather than replacing it entirely, which maintains the contract that other code expects.
Abstract Base Classes and Interface Design
Python’s abc
module lets you define abstract base classes that enforce certain methods must be implemented by subclasses. This is particularly useful when you’re designing frameworks or APIs:
from abc import ABC, abstractmethod
class MediaProcessor(ABC):
@abstractmethod
def process(self, media_item):
"""Process a media item - must be implemented by subclasses"""
pass
@abstractmethod
def get_supported_formats(self):
"""Return list of supported formats"""
pass
def validate_format(self, format_type):
"""Concrete method available to all subclasses"""
return format_type in self.get_supported_formats()
class VideoProcessor(MediaProcessor):
def process(self, media_item):
if isinstance(media_item, Movie):
return f"Processing video: {media_item.title}"
return "Unsupported media type"
def get_supported_formats(self):
return ["mp4", "avi", "mkv"]
class AudioProcessor(MediaProcessor):
def process(self, media_item):
if isinstance(media_item, AudioBook):
return f"Processing audio: {media_item.title}"
return "Unsupported media type"
def get_supported_formats(self):
return ["mp3", "wav", "flac"]
Abstract base classes provide a contract that subclasses must follow, making your code more predictable and easier to maintain. They’re especially valuable in team environments where different developers are implementing different parts of a system.
Composition vs Inheritance
While inheritance is powerful, it’s not always the right solution. Sometimes composition—building objects that contain other objects—provides better flexibility and maintainability. The classic rule is “favor composition over inheritance,” and Python makes both approaches natural:
class MediaMetadata:
def __init__(self, title, creator, year):
self.title = title
self.creator = creator
self.year = year
self.tags = []
def add_tag(self, tag):
if tag not in self.tags:
self.tags.append(tag)
class PlaybackEngine:
def __init__(self):
self.current_position = 0
self.is_playing = False
def play(self):
self.is_playing = True
return "Playback started"
def pause(self):
self.is_playing = False
return "Playback paused"
# Composition: MediaPlayer contains other objects
class MediaPlayer:
def __init__(self, title, creator, year):
self.metadata = MediaMetadata(title, creator, year)
self.engine = PlaybackEngine()
self.playlist = []
def play(self):
return self.engine.play()
def get_title(self):
return self.metadata.title
This composition approach gives you more flexibility than inheritance. You can easily swap out different playback engines or metadata systems without changing the core MediaPlayer
class.
In our next part, we’ll dive into Python’s special methods (magic methods) that let you customize how your objects behave with built-in operations. You’ll learn how to make your classes work seamlessly with Python’s operators, built-in functions, and language constructs.