Lesson 3 - Inheritance and Polymorphism
Introduction
So far, we’ve learned how to create classes with attributes, methods, and properties. But what if we want to create similar classes that share some functionality? For example, what if we’re building a bookstore that sells both physical books and ebooks? They share some properties (title, author, price) but also have differences (physical books have weight and shipping, ebooks have file size and download limits).
Instead of duplicating code, we can use inheritance—creating new classes based on existing ones. This lesson covers:
- How inheritance works in Python
- The
super()function for calling parent methods - Method overriding
- Multiple inheritance
- Polymorphism and why it matters
Basic Inheritance
Inheritance lets you create a new class based on an existing class. The new class (called the child or subclass) inherits all attributes and methods from the existing class (called the parent or superclass).
Let’s start with a simple example:
class Book:
def __init__(self, title, author, price):
self.title = title
self.author = author
self.price = price
def display_info(self):
print(f"'{self.title}' by {self.author}")
print(f"Price: ${self.price}")
# PhysicalBook inherits from Book
class PhysicalBook(Book):
def __init__(self, title, author, price, weight):
super().__init__(title, author, price) # Call parent's __init__
self.weight = weight
def calculate_shipping(self):
# $5 base + $0.50 per pound
return 5 + (self.weight * 0.5)
# Create a physical book
book = PhysicalBook("Python Crash Course", "Eric Matthes", 39.99, 2.5)
# Inherited method from Book
book.display_info()
# Method specific to PhysicalBook
shipping = book.calculate_shipping()
print(f"Shipping cost: ${shipping:.2f}")Output:
'Python Crash Course' by Eric Matthes
Price: $39.99
Shipping cost: $6.25Key points:
class PhysicalBook(Book):creates a subclass ofBooksuper().__init__(...)calls the parent class’s__init__methodPhysicalBookhas access todisplay_info()fromBookPhysicalBookadds its own methodcalculate_shipping()
The super() Function
The super() function gives you access to the parent class without explicitly naming it. This is especially useful if you later change which class you’re inheriting from.
class Book:
def __init__(self, title, author, price):
self.title = title
self.author = author
self.price = price
print(f"Book.__init__ called for '{title}'")
class PhysicalBook(Book):
def __init__(self, title, author, price, weight):
print(f"PhysicalBook.__init__ called")
super().__init__(title, author, price)
self.weight = weight
book = PhysicalBook("Python Basics", "John Doe", 29.99, 1.8)Output:
PhysicalBook.__init__ called
Book.__init__ called for 'Python Basics'Notice how super().__init__() calls the parent’s initialization, letting us extend it rather than completely replace it.
Method Overriding
Method overriding means defining a method in the child class with the same name as a method in the parent class. The child’s version replaces the parent’s version.
class Book:
def __init__(self, title, author, price):
self.title = title
self.author = author
self.price = price
def display_info(self):
print(f"'{self.title}' by {self.author}")
print(f"Price: ${self.price}")
class Ebook(Book):
def __init__(self, title, author, price, file_size_mb):
super().__init__(title, author, price)
self.file_size_mb = file_size_mb
# Override the parent's display_info method
def display_info(self):
print(f"[EBOOK] '{self.title}' by {self.author}")
print(f"Price: ${self.price}")
print(f"File size: {self.file_size_mb} MB")
# Create both types
physical = Book("Python Basics", "John Doe", 29.99)
ebook = Ebook("Python Basics", "John Doe", 19.99, 25.5)
physical.display_info()
print()
ebook.display_info()Output:
'Python Basics' by John Doe
Price: $29.99
[EBOOK] 'Python Basics' by John Doe
Price: $19.99
File size: 25.5 MBThe Ebook class completely replaces display_info(). But sometimes you want to extend the parent’s method rather than replace it:
class Ebook(Book):
def __init__(self, title, author, price, file_size_mb):
super().__init__(title, author, price)
self.file_size_mb = file_size_mb
def display_info(self):
# Call the parent's method first
super().display_info()
# Then add ebook-specific info
print(f"File size: {self.file_size_mb} MB")
print("[Digital Download]")
ebook = Ebook("Python Basics", "John Doe", 19.99, 25.5)
ebook.display_info()Output:
'Python Basics' by John Doe
Price: $19.99
File size: 25.5 MB
[Digital Download]A Practical Example: Product Hierarchy
Let’s build a more realistic example for a bookstore that sells different types of products:
class Product:
"""Base class for all products"""
def __init__(self, title, price):
self.title = title
self.price = price
def display_info(self):
print(f"\n{self.title}")
print(f"Price: ${self.price:.2f}")
def get_final_price(self):
"""Base method - can be overridden by subclasses"""
return self.price
class Book(Product):
"""Physical books with shipping"""
def __init__(self, title, author, price, pages):
super().__init__(title, price)
self.author = author
self.pages = pages
def display_info(self):
super().display_info()
print(f"Author: {self.author}")
print(f"Pages: {self.pages}")
def get_final_price(self):
# Add shipping for physical books
shipping = 5.99
return self.price + shipping
class Ebook(Product):
"""Digital books - no shipping, cheaper base price"""
def __init__(self, title, author, price, file_size_mb):
super().__init__(title, price)
self.author = author
self.file_size_mb = file_size_mb
def display_info(self):
super().display_info()
print(f"Author: {self.author}")
print(f"File size: {self.file_size_mb} MB")
print("[Digital Download - No Shipping]")
def get_final_price(self):
# No shipping for ebooks
return self.price
class AudioBook(Product):
"""Audio books with duration"""
def __init__(self, title, narrator, price, duration_hours):
super().__init__(title, price)
self.narrator = narrator
self.duration_hours = duration_hours
def display_info(self):
super().display_info()
print(f"Narrated by: {self.narrator}")
print(f"Duration: {self.duration_hours} hours")
print("[Digital Audio - No Shipping]")
def get_final_price(self):
# No shipping for audio books
return self.price
# Create different products
products = [
Book("Python Crash Course", "Eric Matthes", 39.99, 544),
Ebook("Automate the Boring Stuff", "Al Sweigart", 19.99, 25.5),
AudioBook("Clean Code", "Robert C. Martin", 29.99, 12.5)
]
# Process all products the same way
total = 0
for product in products:
product.display_info()
final_price = product.get_final_price()
print(f"Final price: ${final_price:.2f}")
total += final_price
print(f"\nTotal cart value: ${total:.2f}")Output:
Python Crash Course
Price: $39.99
Author: Eric Matthes
Pages: 544
Final price: $45.98
Automate the Boring Stuff
Price: $19.99
Author: Al Sweigart
File size: 25.5 MB
[Digital Download - No Shipping]
Final price: $19.99
Clean Code
Price: $29.99
Narrated by: Robert C. Martin
Duration: 12.5 hours
[Digital Audio - No Shipping]
Final price: $29.99
Total cart value: $95.96Notice how we can treat all products the same way in the loop, but each type behaves differently. This is polymorphism in action.
Polymorphism
Polymorphism means “many forms.” In OOP, it means that objects of different classes can be used interchangeably if they share a common interface (the same method names).
class Product:
def __init__(self, title, price):
self.title = title
self.price = price
def get_description(self):
return f"{self.title} - ${self.price}"
class Book(Product):
def __init__(self, title, price, author):
super().__init__(title, price)
self.author = author
def get_description(self):
return f"{self.title} by {self.author} - ${self.price}"
class Ebook(Product):
def __init__(self, title, price, file_size):
super().__init__(title, price)
self.file_size = file_size
def get_description(self):
return f"[EBOOK] {self.title} ({self.file_size}MB) - ${self.price}"
def print_catalog(products):
"""This function works with any product type"""
for product in products:
# Each product type implements get_description() differently
print(product.get_description())
# Create different product types
catalog = [
Product("Gift Card", 50),
Book("Python Basics", 29.99, "John Doe"),
Ebook("Advanced Python", 39.99, 30)
]
print_catalog(catalog)Output:
Gift Card - $50
Python Basics by John Doe - $29.99
[EBOOK] Advanced Python (30MB) - $39.99The print_catalog() function doesn’t care what type of product it receives—it just calls get_description(). This is polymorphism: different objects respond to the same method call in their own way.
Checking Class Relationships
Python provides built-in functions to check class relationships:
class Product:
pass
class Book(Product):
pass
class Ebook(Book):
pass
ebook = Ebook()
# isinstance() checks if an object is an instance of a class
print(isinstance(ebook, Ebook)) # True
print(isinstance(ebook, Book)) # True (Ebook inherits from Book)
print(isinstance(ebook, Product)) # True (Book inherits from Product)
print(isinstance(ebook, str)) # False
# issubclass() checks if a class is a subclass of another
print(issubclass(Ebook, Book)) # True
print(issubclass(Ebook, Product)) # True
print(issubclass(Book, Ebook)) # FalseMultiple Inheritance
Python supports multiple inheritance—a class can inherit from multiple parent classes. Use this carefully, as it can get complex.
class Discountable:
"""Mixin for products that can be discounted"""
def apply_discount(self, discount_percent):
self.price = self.price * (1 - discount_percent / 100)
return self.price
class Reviewable:
"""Mixin for products that can be reviewed"""
def __init__(self):
self.reviews = []
def add_review(self, rating, comment):
self.reviews.append({'rating': rating, 'comment': comment})
def get_average_rating(self):
if not self.reviews:
return 0
total = sum(review['rating'] for review in self.reviews)
return total / len(self.reviews)
class Book(Discountable, Reviewable):
"""Book that can be discounted and reviewed"""
def __init__(self, title, author, price):
self.title = title
self.author = author
self.price = price
Reviewable.__init__(self) # Initialize reviews list
def display_info(self):
avg_rating = self.get_average_rating()
print(f"\n{self.title} by {self.author}")
print(f"Price: ${self.price:.2f}")
if avg_rating > 0:
print(f"Average rating: {avg_rating:.1f}/5.0")
# Create and use a book with multiple capabilities
book = Book("Python Crash Course", "Eric Matthes", 39.99)
# From Reviewable
book.add_review(5, "Excellent for beginners!")
book.add_review(4, "Very comprehensive")
# From Discountable
book.apply_discount(20)
# From Book
book.display_info()Output:
Python Crash Course by Eric Matthes
Price: $31.99
Average rating: 4.5/5.0Note: With multiple inheritance, be careful about the Method Resolution Order (MRO)—the order Python looks for methods. You can check it with ClassName.__mro__ or ClassName.mro().
Abstract Base Classes (Preview)
Sometimes you want to create a parent class that should never be instantiated directly—it only exists to be inherited from. Python’s abc module provides this:
from abc import ABC, abstractmethod
class Product(ABC):
"""Abstract base class - cannot be instantiated"""
def __init__(self, title, price):
self.title = title
self.price = price
@abstractmethod
def get_final_price(self):
"""All products must implement this method"""
pass
@abstractmethod
def display_info(self):
"""All products must implement this method"""
pass
class Book(Product):
def __init__(self, title, price, author):
super().__init__(title, price)
self.author = author
def get_final_price(self):
return self.price + 5.99 # Add shipping
def display_info(self):
print(f"{self.title} by {self.author} - ${self.price}")
# This works - Book implements all abstract methods
book = Book("Python Basics", 29.99, "John Doe")
book.display_info()
# This would raise an error - Product is abstract
# product = Product("Test", 10) # TypeError!Abstract base classes enforce that subclasses implement certain methods, making your code more robust.
Summary
In this lesson, we learned about inheritance and polymorphism:
- Inheritance lets classes build on other classes, reusing code and creating hierarchies
- The
super()function calls parent class methods - Method overriding lets child classes replace or extend parent methods
- Polymorphism lets different objects respond to the same method calls in their own way
- Multiple inheritance lets a class inherit from multiple parents (use carefully)
- Abstract base classes define interfaces that subclasses must implement
These concepts help us:
- Avoid code duplication
- Create logical class hierarchies
- Write flexible code that works with many types
- Enforce consistent interfaces across related classes
In the next lesson, we’ll explore special methods (also called “magic methods” or “dunder methods”) that let us customize how objects behave with Python’s built-in operations.