Python - Inheritance with Examples
Inheritance creates an 'is-a' relationship between classes. A child class inherits all attributes and methods from its parent, then extends or modifies behavior as needed.
Key Insights
- Inheritance enables code reuse by allowing child classes to inherit attributes and methods from parent classes, reducing duplication and creating logical hierarchies
- Python supports multiple inheritance with Method Resolution Order (MRO) using C3 linearization to determine which parent class method gets called
- The super() function provides clean access to parent class methods, especially critical in multiple inheritance scenarios where explicit parent references can cause issues
Understanding Basic Inheritance
Inheritance creates an “is-a” relationship between classes. A child class inherits all attributes and methods from its parent, then extends or modifies behavior as needed.
class Vehicle:
def __init__(self, brand, model):
self.brand = brand
self.model = model
self.odometer = 0
def drive(self, miles):
self.odometer += miles
print(f"Drove {miles} miles. Total: {self.odometer}")
def description(self):
return f"{self.brand} {self.model}"
class ElectricCar(Vehicle):
def __init__(self, brand, model, battery_capacity):
super().__init__(brand, model)
self.battery_capacity = battery_capacity
self.charge_level = 100
def charge(self, percent):
self.charge_level = min(100, self.charge_level + percent)
print(f"Battery charged to {self.charge_level}%")
def drive(self, miles):
# Override parent method with electric-specific logic
battery_used = miles * 0.3
if self.charge_level >= battery_used:
self.charge_level -= battery_used
super().drive(miles)
else:
print("Insufficient battery charge")
tesla = ElectricCar("Tesla", "Model 3", 75)
print(tesla.description()) # Inherited method
tesla.drive(50) # Overridden method
tesla.charge(20) # New method
The super() function calls the parent class constructor, ensuring proper initialization. Without it, the brand, model, and odometer attributes wouldn’t be set.
Method Overriding and Extension
Child classes can completely replace parent methods or extend them with additional functionality.
class Logger:
def log(self, message):
print(f"[LOG] {message}")
class TimestampLogger(Logger):
def log(self, message):
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Extend parent behavior
super().log(f"[{timestamp}] {message}")
class FileLogger(Logger):
def __init__(self, filename):
self.filename = filename
def log(self, message):
# Completely replace parent behavior
with open(self.filename, 'a') as f:
f.write(f"{message}\n")
class VerboseFileLogger(FileLogger):
def log(self, message):
# Extend child behavior
verbose_msg = f"VERBOSE: {message}"
super().log(verbose_msg)
logger = TimestampLogger()
logger.log("Application started")
file_logger = VerboseFileLogger("app.log")
file_logger.log("Error occurred")
Multiple Inheritance
Python allows inheriting from multiple parent classes. The Method Resolution Order (MRO) determines which parent’s method gets called.
class Flyable:
def fly(self):
return "Flying through the air"
def move(self):
return "Moving by flying"
class Swimmable:
def swim(self):
return "Swimming through water"
def move(self):
return "Moving by swimming"
class Duck(Flyable, Swimmable):
def quack(self):
return "Quack!"
duck = Duck()
print(duck.fly()) # Flying through the air
print(duck.swim()) # Swimming through water
print(duck.move()) # Moving by flying (Flyable comes first)
print(Duck.__mro__) # Shows method resolution order
The MRO follows the order: Duck → Flyable → Swimmable → object. When move() is called, Python finds it first in Flyable.
Diamond Problem and Super()
The diamond problem occurs when a class inherits from two classes that share a common ancestor. Python’s super() handles this elegantly.
class Animal:
def __init__(self, name):
print(f"Animal.__init__({name})")
self.name = name
class Mammal(Animal):
def __init__(self, name, warm_blooded=True):
print(f"Mammal.__init__({name})")
super().__init__(name)
self.warm_blooded = warm_blooded
class WingedAnimal(Animal):
def __init__(self, name, wingspan):
print(f"WingedAnimal.__init__({name})")
super().__init__(name)
self.wingspan = wingspan
class Bat(Mammal, WingedAnimal):
def __init__(self, name, wingspan):
print(f"Bat.__init__({name})")
super().__init__(name, wingspan=wingspan)
# Create a bat and observe initialization order
bat = Bat("Dracula", 30)
print(f"\nMRO: {[cls.__name__ for cls in Bat.__mro__]}")
Output shows super() ensures Animal.__init__ is called only once:
Bat.__init__(Dracula)
Mammal.__init__(Dracula)
WingedAnimal.__init__(Dracula)
Animal.__init__(Dracula)
MRO: ['Bat', 'Mammal', 'WingedAnimal', 'Animal', 'object']
Abstract Base Classes
Abstract base classes enforce that child classes implement specific methods, creating contracts for inheritance.
from abc import ABC, abstractmethod
class DataStorage(ABC):
@abstractmethod
def save(self, data):
pass
@abstractmethod
def load(self):
pass
def validate(self, data):
# Concrete method available to all children
return len(data) > 0
class DatabaseStorage(DataStorage):
def __init__(self, connection_string):
self.connection_string = connection_string
def save(self, data):
print(f"Saving to database: {data}")
def load(self):
print("Loading from database")
return {"id": 1, "value": "data"}
class FileStorage(DataStorage):
def __init__(self, filepath):
self.filepath = filepath
def save(self, data):
with open(self.filepath, 'w') as f:
f.write(str(data))
def load(self):
with open(self.filepath, 'r') as f:
return f.read()
# This would raise TypeError: Can't instantiate abstract class
# storage = DataStorage()
db = DatabaseStorage("postgresql://localhost")
db.save({"user": "john"})
Composition vs Inheritance
Not every relationship requires inheritance. Composition (“has-a”) often provides better flexibility than inheritance (“is-a”).
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self):
return f"{self.horsepower}hp engine started"
class GPS:
def navigate(self, destination):
return f"Navigating to {destination}"
# Composition: Car HAS-A engine and GPS
class Car:
def __init__(self, brand, engine, has_gps=False):
self.brand = brand
self.engine = engine
self.gps = GPS() if has_gps else None
def start(self):
return self.engine.start()
def navigate(self, destination):
if self.gps:
return self.gps.navigate(destination)
return "No GPS available"
engine = Engine(300)
car = Car("BMW", engine, has_gps=True)
print(car.start())
print(car.navigate("New York"))
Composition allows mixing and matching components without complex inheritance hierarchies.
Property Inheritance and Overriding
Properties inherit and can be overridden like methods, enabling custom getter/setter logic in child classes.
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
return (self.celsius * 9/5) + 32
class ValidatedTemperature(Temperature):
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero")
if value > 1000:
raise ValueError("Temperature unreasonably high")
self._celsius = value
temp = ValidatedTemperature(25)
print(f"{temp.celsius}°C = {temp.fahrenheit}°F")
try:
temp.celsius = 1500 # Raises ValueError
except ValueError as e:
print(f"Error: {e}")
Practical Example: Plugin System
Inheritance enables extensible plugin architectures where new functionality can be added without modifying existing code.
class Plugin(ABC):
@abstractmethod
def execute(self, context):
pass
@property
@abstractmethod
def name(self):
pass
class ValidationPlugin(Plugin):
@property
def name(self):
return "Validator"
def execute(self, context):
if 'data' not in context:
raise ValueError("Missing data in context")
return {"valid": len(context['data']) > 0}
class TransformPlugin(Plugin):
@property
def name(self):
return "Transformer"
def execute(self, context):
data = context.get('data', '')
return {"transformed": data.upper()}
class PluginManager:
def __init__(self):
self.plugins = []
def register(self, plugin):
if not isinstance(plugin, Plugin):
raise TypeError("Must be a Plugin instance")
self.plugins.append(plugin)
def execute_all(self, context):
results = {}
for plugin in self.plugins:
results[plugin.name] = plugin.execute(context)
return results
manager = PluginManager()
manager.register(ValidationPlugin())
manager.register(TransformPlugin())
results = manager.execute_all({'data': 'hello world'})
print(results)
This pattern allows adding new plugins by creating new classes that inherit from Plugin, without modifying PluginManager.