Welcome to the comprehensive guide to Python Inheritance! This tutorial covers one of the most powerful features of Object-Oriented Programming, designed for learners from beginner to expert level.
- Introduction to Inheritance
- What is Inheritance?
- Basic Inheritance Syntax
- The super() Function
- Method Overriding
- Types of Inheritance
- Abstract Base Classes
- Multiple Inheritance
- Method Resolution Order (MRO)
- Advanced Inheritance Concepts
- Best Practices
- Common Pitfalls and Solutions
- Real-World Examples
- Exercises and Practice
Inheritance is a fundamental concept in Object-Oriented Programming that allows a class to inherit attributes and methods from another class. It enables code reuse and establishes a hierarchical relationship between classes.
Real-world analogy:
- A child inherits traits from their parents (eye color, height, etc.)
- A car inherits properties from a general vehicle (wheels, engine, etc.)
- A smartphone inherits features from a general phone (calling, messaging, etc.)
- Code Reusability: Write common functionality once in a parent class
- Hierarchical Organization: Model real-world relationships naturally
- Polymorphism: Treat different objects uniformly through a common interface
- Extensibility: Add new functionality while preserving existing behavior
- Maintainability: Changes to common functionality affect all subclasses
- Parent Class (Base Class, Superclass): The class being inherited from
- Child Class (Derived Class, Subclass): The class that inherits
- Inheritance: The mechanism of creating new classes based on existing ones
- Override: Replacing a parent method with a new implementation
- Extend: Adding new functionality to inherited behavior
Inheritance represents an "is-a" relationship between classes:
# Good examples of "is-a" relationships
class Animal:
pass
class Dog(Animal): # A Dog IS-A Animal
pass
class Car(Vehicle): # A Car IS-A Vehicle
pass
class Manager(Employee): # A Manager IS-A Employee
passInheritance ("is-a") vs Composition ("has-a"):
# INHERITANCE - "is-a" relationship
class Vehicle:
def __init__(self, make, model):
self.make = make
self.model = model
class Car(Vehicle): # Car IS-A Vehicle
def __init__(self, make, model, doors):
super().__init__(make, model)
self.doors = doors
# COMPOSITION - "has-a" relationship
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
class Car: # Car HAS-A Engine
def __init__(self, make, model, engine):
self.make = make
self.model = model
self.engine = engine # Composition# Animal Kingdom Hierarchy
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species
def eat(self):
return f"{self.name} is eating"
def sleep(self):
return f"{self.name} is sleeping"
class Mammal(Animal):
def __init__(self, name, species, fur_color):
super().__init__(name, species)
self.fur_color = fur_color
def give_birth(self):
return f"{self.name} gives birth to live young"
class Dog(Mammal):
def __init__(self, name, breed, fur_color):
super().__init__(name, "Canine", fur_color)
self.breed = breed
def bark(self):
return f"{self.name} says Woof!"
def fetch(self):
return f"{self.name} fetches the ball"
# Usage
my_dog = Dog("Buddy", "Golden Retriever", "Golden")
print(my_dog.eat()) # Inherited from Animal
print(my_dog.give_birth()) # Inherited from Mammal
print(my_dog.bark()) # Specific to Dog
print(my_dog.fetch()) # Specific to Dog# Parent class (Base class)
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def introduce(self):
return f"Hi, I'm {self.name} and I'm {self.age} years old"
def have_birthday(self):
self.age += 1
return f"Happy birthday! Now {self.age} years old"
# Child class (Derived class)
class Student(Person): # Student inherits from Person
def __init__(self, name, age, student_id):
super().__init__(name, age) # Call parent constructor
self.student_id = student_id
self.grades = []
def add_grade(self, grade):
self.grades.append(grade)
def get_gpa(self):
if not self.grades:
return 0.0
return sum(self.grades) / len(self.grades)
# Usage
student = Student("Alice", 20, "S12345")
print(student.introduce()) # Inherited method
print(student.have_birthday()) # Inherited method
student.add_grade(85) # New method
student.add_grade(92) # New method
print(f"GPA: {student.get_gpa()}") # New method# Check if an object is an instance of a class
print(isinstance(student, Student)) # True
print(isinstance(student, Person)) # True (inheritance)
print(isinstance(student, str)) # False
# Check if a class is a subclass of another
print(issubclass(Student, Person)) # True
print(issubclass(Person, Student)) # False
print(issubclass(Student, object)) # True (everything inherits from object)
# Get the method resolution order
print(Student.__mro__)
# (<class '__main__.Student'>, <class '__main__.Person'>, <class 'object'>)The super() function provides access to methods in a parent class from a child class. It's essential for proper inheritance implementation.
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
print(f"Vehicle created: {make} {model} ({year})")
def start(self):
return f"The {self.make} {self.model} is starting..."
def stop(self):
return f"The {self.make} {self.model} has stopped."
class Car(Vehicle):
def __init__(self, make, model, year, doors):
# Call parent constructor
super().__init__(make, model, year)
self.doors = doors
print(f"Car-specific setup: {doors} doors")
def start(self):
# Extend parent method
parent_result = super().start()
return f"{parent_result} Engine running smoothly!"
def honk(self):
return f"The {self.make} {self.model} goes BEEP BEEP!"
# Usage
car = Car("Toyota", "Camry", 2025, 4)
print(car.start()) # Uses both parent and child logic
print(car.honk()) # Child-specific methodclass Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
self.benefits = []
class Manager(Employee):
def __init__(self, name, salary, department):
super().__init__(name, salary) # Initialize parent attributes
self.department = department
self.team_size = 0class BankAccount:
def __init__(self, account_number, balance=0):
self.account_number = account_number
self.balance = balance
self.transaction_history = []
def deposit(self, amount):
self.balance += amount
self.transaction_history.append(f"Deposit: +${amount}")
return self.balance
class SavingsAccount(BankAccount):
def __init__(self, account_number, balance=0, interest_rate=0.02):
super().__init__(account_number, balance)
self.interest_rate = interest_rate
def deposit(self, amount):
# Call parent deposit method first
new_balance = super().deposit(amount)
# Add savings-specific logic
if amount >= 1000:
bonus = amount * 0.001 # 0.1% bonus for large deposits
self.balance += bonus
self.transaction_history.append(f"Bonus: +${bonus:.2f}")
return self.balanceclass A:
def method(self):
print("A.method")
class B(A):
def method(self):
print("B.method")
super().method() # Calls A.method
class C(A):
def method(self):
print("C.method")
super().method() # Calls A.method
class D(B, C): # Multiple inheritance
def method(self):
print("D.method")
super().method() # Follows MRO: B -> C -> A
# Usage
d = D()
d.method()
# Output:
# D.method
# B.method
# C.method
# A.methodclass Parent:
def greet(self):
return "Hello from Parent"
class Child(Parent):
def greet(self):
# ✅ Good - Uses super()
return f"Child says: {super().greet()}"
def greet_bad(self):
# ❌ Bad - Direct parent call
return f"Child says: {Parent.greet(self)}"
# Why super() is better:
# 1. Works correctly with multiple inheritance
# 2. Automatically handles method resolution order
# 3. More maintainable if parent class changesMethod overriding allows a child class to provide a specific implementation of a method that is already defined in its parent class.
class Animal:
def __init__(self, name):
self.name = name
def make_sound(self):
return f"{self.name} makes a generic animal sound"
def move(self):
return f"{self.name} moves around"
class Dog(Animal):
def make_sound(self): # Override parent method
return f"{self.name} barks: Woof! Woof!"
def move(self): # Override parent method
return f"{self.name} runs on four legs"
class Bird(Animal):
def make_sound(self): # Override parent method
return f"{self.name} chirps: Tweet! Tweet!"
def move(self): # Override parent method
return f"{self.name} flies through the air"
# Polymorphism in action
animals = [Dog("Buddy"), Bird("Tweety"), Dog("Rex")]
for animal in animals:
print(animal.make_sound()) # Different behavior for each type
print(animal.move())class Shape:
def __init__(self, color):
self.color = color
def describe(self):
return f"This is a {self.color} shape"
class Circle(Shape):
def __init__(self, color, radius):
super().__init__(color)
self.radius = radius
def describe(self):
# Extend parent method
base_description = super().describe()
return f"{base_description} with radius {self.radius}"
def area(self):
return 3.14159 * self.radius ** 2
class Rectangle(Shape):
def __init__(self, color, width, height):
super().__init__(color)
self.width = width
self.height = height
def describe(self):
# Extend parent method
base_description = super().describe()
return f"{base_description} with dimensions {self.width}x{self.height}"
def area(self):
return self.width * self.height
# Usage
circle = Circle("red", 5)
rectangle = Rectangle("blue", 4, 6)
print(circle.describe()) # Extended description
print(f"Area: {circle.area()}")
print(rectangle.describe()) # Extended description
print(f"Area: {rectangle.area()}")class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"Person(name='{self.name}', age={self.age})"
def __repr__(self):
return f"Person('{self.name}', {self.age})"
class Employee(Person):
def __init__(self, name, age, employee_id, salary):
super().__init__(name, age)
self.employee_id = employee_id
self.salary = salary
def __str__(self): # Override string representation
return f"Employee(name='{self.name}', id={self.employee_id}, salary=${self.salary})"
def __repr__(self): # Override developer representation
return f"Employee('{self.name}', {self.age}, '{self.employee_id}', {self.salary})"
def __eq__(self, other): # Override equality comparison
if isinstance(other, Employee):
return self.employee_id == other.employee_id
return False
# Usage
emp1 = Employee("Alice", 30, "E001", 75000)
emp2 = Employee("Bob", 25, "E002", 65000)
emp3 = Employee("Alice", 31, "E001", 80000) # Same ID as emp1
print(str(emp1)) # Uses overridden __str__
print(repr(emp1)) # Uses overridden __repr__
print(emp1 == emp3) # True (same employee_id)
print(emp1 == emp2) # False (different employee_id)One child class inherits from one parent class.
class Animal:
def breathe(self):
return "Breathing..."
class Dog(Animal): # Single inheritance
def bark(self):
return "Woof!"
dog = Dog()
print(dog.breathe()) # Inherited
print(dog.bark()) # Own methodOne child class inherits from multiple parent classes.
class Flyable:
def fly(self):
return "Flying through the air"
class Swimmable:
def swim(self):
return "Swimming in water"
class Duck(Flyable, Swimmable): # Multiple inheritance
def quack(self):
return "Quack quack!"
duck = Duck()
print(duck.fly()) # From Flyable
print(duck.swim()) # From Swimmable
print(duck.quack()) # Own methodA chain of inheritance: A -> B -> C
class Vehicle:
def __init__(self, make, model):
self.make = make
self.model = model
def start(self):
return "Vehicle starting..."
class Car(Vehicle): # Car inherits from Vehicle
def __init__(self, make, model, doors):
super().__init__(make, model)
self.doors = doors
def drive(self):
return "Driving on roads"
class SportsCar(Car): # SportsCar inherits from Car
def __init__(self, make, model, doors, top_speed):
super().__init__(make, model, doors)
self.top_speed = top_speed
def race(self):
return f"Racing at {self.top_speed} mph!"
# SportsCar has access to all methods in the chain
sports_car = SportsCar("Ferrari", "F8", 2, 211)
print(sports_car.start()) # From Vehicle
print(sports_car.drive()) # From Car
print(sports_car.race()) # Own methodMultiple child classes inherit from one parent class.
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def work(self):
return f"{self.name} is working"
class Developer(Employee): # Inherits from Employee
def code(self):
return f"{self.name} is coding"
class Manager(Employee): # Inherits from Employee
def manage(self):
return f"{self.name} is managing"
class Designer(Employee): # Inherits from Employee
def design(self):
return f"{self.name} is designing"
# All inherit from Employee but have different specializations
dev = Developer("Alice", 80000)
mgr = Manager("Bob", 90000)
des = Designer("Carol", 70000)
print(dev.work()) # Inherited
print(dev.code()) # Specific to Developer
print(mgr.manage()) # Specific to Manager
print(des.design()) # Specific to DesignerCombination of multiple inheritance types.
class Animal:
def __init__(self, name):
self.name = name
def eat(self):
return f"{self.name} is eating"
class Mammal(Animal):
def give_birth(self):
return f"{self.name} gives birth to live young"
class Bird(Animal):
def lay_eggs(self):
return f"{self.name} lays eggs"
class Flyable:
def fly(self):
return f"Flying high in the sky"
class Bat(Mammal, Flyable): # Multiple + Multilevel
def echolocate(self):
return f"{self.name} uses echolocation"
class Penguin(Bird): # Multilevel
def swim(self):
return f"{self.name} swims in cold water"
# Bat inherits from both Mammal (which inherits from Animal) and Flyable
bat = Bat("Bruce")
print(bat.eat()) # From Animal (via Mammal)
print(bat.give_birth()) # From Mammal
print(bat.fly()) # From Flyable
print(bat.echolocate()) # Own methodAbstract Base Classes (ABCs) define a contract that subclasses must follow. They cannot be instantiated directly and typically contain abstract methods that must be implemented by subclasses.
from abc import ABC, abstractmethod
class Shape(ABC): # Abstract base class
def __init__(self, color):
self.color = color
@abstractmethod
def area(self):
"""Calculate the area of the shape"""
pass
@abstractmethod
def perimeter(self):
"""Calculate the perimeter of the shape"""
pass
# Concrete method (can be inherited as-is)
def describe(self):
return f"This is a {self.color} {self.__class__.__name__.lower()}"
class Circle(Shape):
def __init__(self, color, radius):
super().__init__(color)
self.radius = radius
def area(self): # Must implement abstract method
return 3.14159 * self.radius ** 2
def perimeter(self): # Must implement abstract method
return 2 * 3.14159 * self.radius
class Rectangle(Shape):
def __init__(self, color, width, height):
super().__init__(color)
self.width = width
self.height = height
def area(self): # Must implement abstract method
return self.width * self.height
def perimeter(self): # Must implement abstract method
return 2 * (self.width + self.height)
# Usage
# shape = Shape("red") # ❌ TypeError: Can't instantiate abstract class
circle = Circle("red", 5)
rectangle = Rectangle("blue", 4, 6)
print(circle.describe()) # Inherited concrete method
print(f"Area: {circle.area()}")
print(f"Perimeter: {circle.perimeter()}")
print(rectangle.describe()) # Inherited concrete method
print(f"Area: {rectangle.area()}")
print(f"Perimeter: {rectangle.perimeter()}")from abc import ABC, abstractmethod
class Vehicle(ABC):
def __init__(self, make, model):
self.make = make
self.model = model
@property
@abstractmethod
def max_speed(self):
"""Maximum speed of the vehicle"""
pass
@property
@abstractmethod
def fuel_type(self):
"""Type of fuel used"""
pass
@abstractmethod
def start_engine(self):
"""Start the vehicle's engine"""
pass
class Car(Vehicle):
def __init__(self, make, model, max_speed, fuel_type):
super().__init__(make, model)
self._max_speed = max_speed
self._fuel_type = fuel_type
@property
def max_speed(self):
return self._max_speed
@property
def fuel_type(self):
return self._fuel_type
def start_engine(self):
return f"Starting {self.make} {self.model} engine with {self.fuel_type}"
class Bicycle(Vehicle):
def __init__(self, make, model):
super().__init__(make, model)
@property
def max_speed(self):
return 30 # km/h
@property
def fuel_type(self):
return "Human power"
def start_engine(self):
return f"No engine to start on {self.make} {self.model} - just pedal!"
# Usage
car = Car("Toyota", "Camry", 180, "Gasoline")
bike = Bicycle("Trek", "Mountain Bike")
print(f"Car max speed: {car.max_speed} km/h")
print(car.start_engine())
print(f"Bike max speed: {bike.max_speed} km/h")
print(bike.start_engine())from abc import ABC, abstractmethod
class DataProcessor(ABC):
"""Template for data processing algorithms"""
def process(self, data):
"""Template method defining the algorithm structure"""
print("Starting data processing...")
# Step 1: Validate data
if not self.validate_data(data):
raise ValueError("Invalid data")
# Step 2: Process data (abstract - must be implemented)
processed_data = self.process_data(data)
# Step 3: Save results (abstract - must be implemented)
self.save_results(processed_data)
print("Data processing completed!")
return processed_data
def validate_data(self, data):
"""Default validation - can be overridden"""
return data is not None and len(data) > 0
@abstractmethod
def process_data(self, data):
"""Process the data - must be implemented by subclasses"""
pass
@abstractmethod
def save_results(self, processed_data):
"""Save the results - must be implemented by subclasses"""
pass
class CSVProcessor(DataProcessor):
def process_data(self, data):
# Simulate CSV processing
processed = [row.upper() for row in data]
print(f"Processed {len(processed)} CSV rows")
return processed
def save_results(self, processed_data):
print(f"Saving {len(processed_data)} rows to CSV file")
class JSONProcessor(DataProcessor):
def process_data(self, data):
# Simulate JSON processing
processed = {f"item_{i}": item for i, item in enumerate(data)}
print(f"Processed {len(processed)} JSON items")
return processed
def save_results(self, processed_data):
print(f"Saving {len(processed_data)} items to JSON file")
# Usage
csv_processor = CSVProcessor()
json_processor = JSONProcessor()
data = ["apple", "banana", "cherry"]
csv_result = csv_processor.process(data)
json_result = json_processor.process(data)Multiple inheritance allows a class to inherit from more than one parent class, combining functionality from multiple sources.
class Swimmer:
def swim(self):
return "Swimming in water"
def dive(self):
return "Diving underwater"
class Flyer:
def fly(self):
return "Flying in the air"
def land(self):
return "Landing on ground"
class Duck(Swimmer, Flyer): # Multiple inheritance
def __init__(self, name):
self.name = name
def quack(self):
return f"{self.name} says quack!"
# Duck has access to methods from both parent classes
duck = Duck("Donald")
print(duck.swim()) # From Swimmer
print(duck.fly()) # From Flyer
print(duck.quack()) # Own method
print(duck.dive()) # From Swimmer
print(duck.land()) # From FlyerThe diamond problem occurs when a class inherits from two classes that have a common base class.
class A:
def method(self):
print("A.method")
class B(A):
def method(self):
print("B.method")
super().method()
class C(A):
def method(self):
print("C.method")
super().method()
class D(B, C): # Diamond inheritance: D -> B -> A, D -> C -> A
def method(self):
print("D.method")
super().method()
# Check the Method Resolution Order
print("MRO:", D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
# Call the method
d = D()
d.method()
# Output:
# D.method
# B.method
# C.method
# A.methodclass Readable:
def read(self):
return "Reading data..."
def get_size(self):
return "Getting size..."
class Writable:
def write(self, data):
return f"Writing: {data}"
def flush(self):
return "Flushing buffer..."
class Seekable:
def seek(self, position):
return f"Seeking to position {position}"
def tell(self):
return "Current position: 0"
class File(Readable, Writable, Seekable):
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.position = 0
def open(self):
return f"Opening {self.filename} in {self.mode} mode"
def close(self):
return f"Closing {self.filename}"
# File has capabilities from all parent classes
file = File("data.txt", "rw")
print(file.open()) # Own method
print(file.read()) # From Readable
print(file.write("Hello")) # From Writable
print(file.seek(10)) # From Seekable
print(file.close()) # Own methodMixins are classes designed to be mixed in with other classes via multiple inheritance.
class TimestampMixin:
"""Mixin to add timestamp functionality"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
from datetime import datetime
self.created_at = datetime.now()
self.updated_at = datetime.now()
def touch(self):
"""Update the timestamp"""
from datetime import datetime
self.updated_at = datetime.now()
class LoggingMixin:
"""Mixin to add logging functionality"""
def log(self, message):
print(f"[{self.__class__.__name__}] {message}")
class ValidationMixin:
"""Mixin to add validation functionality"""
def validate(self):
"""Override this method in subclasses"""
return True
def is_valid(self):
try:
return self.validate()
except Exception as e:
self.log(f"Validation error: {e}")
return False
class User(TimestampMixin, LoggingMixin, ValidationMixin):
def __init__(self, username, email):
self.username = username
self.email = email
super().__init__() # Initialize mixins
def validate(self):
if not self.username:
raise ValueError("Username is required")
if "@" not in self.email:
raise ValueError("Invalid email format")
return True
def update_email(self, new_email):
self.email = new_email
self.touch() # From TimestampMixin
self.log(f"Email updated to {new_email}") # From LoggingMixin
# Usage
user = User("alice", "alice@example.com")
print(f"User created at: {user.created_at}")
print(f"Is valid: {user.is_valid()}")
user.update_email("alice.smith@example.com")
print(f"User updated at: {user.updated_at}")Method Resolution Order determines the order in which Python searches for methods in inheritance hierarchies, especially important with multiple inheritance.
Python uses the C3 linearization algorithm to determine MRO, which ensures:
- Children are checked before parents
- Parents are checked in the order they appear in the class definition
- A class appears only once in the MRO
class A:
def method(self):
print("A")
class B(A):
def method(self):
print("B")
class C(A):
def method(self):
print("C")
class D(B, C):
def method(self):
print("D")
# Check MRO
print("D MRO:", D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
# Method resolution follows MRO
d = D()
d.method() # Prints "D" (D's method is called first)class Animal:
def move(self):
print("Animal moves")
class Mammal(Animal):
def move(self):
print("Mammal walks")
class Bird(Animal):
def move(self):
print("Bird flies")
class Bat(Mammal, Bird): # Multiple inheritance
pass
class Vampire(Bat):
def move(self):
print("Vampire transforms")
super().move() # Follows MRO
# Check MRO
print("Vampire MRO:")
for i, cls in enumerate(Vampire.__mro__):
print(f" {i}: {cls.__name__}")
# Output:
# 0: Vampire
# 1: Bat
# 2: Mammal
# 3: Bird
# 4: Animal
# 5: object
vampire = Vampire()
vampire.move()
# Output:
# Vampire transforms
# Mammal walksclass LoggerMixin:
def log(self, message):
print(f"[LOG] {message}")
class Base:
def __init__(self, name):
self.name = name
print(f"Base.__init__({name})")
class A(Base):
def __init__(self, name, a_param):
print(f"A.__init__({name}, {a_param})")
super().__init__(name)
self.a_param = a_param
class B(Base):
def __init__(self, name, b_param):
print(f"B.__init__({name}, {b_param})")
super().__init__(name)
self.b_param = b_param
class C(LoggerMixin, A, B):
def __init__(self, name, a_param, b_param, c_param):
print(f"C.__init__({name}, {a_param}, {b_param}, {c_param})")
# This will call A.__init__, which calls B.__init__, which calls Base.__init__
super().__init__(name, a_param)
self.b_param = b_param # Set B's parameter manually
self.c_param = c_param
# Check MRO
print("C MRO:", [cls.__name__ for cls in C.__mro__])
# Create instance
c = C("test", "a_val", "b_val", "c_val")
c.log("Object created successfully")Understanding when to use inheritance vs composition is crucial for good design.
# INHERITANCE - "is-a" relationship
class Vehicle:
def __init__(self, make, model):
self.make = make
self.model = model
def start(self):
return f"Starting {self.make} {self.model}"
class Car(Vehicle): # Car IS-A Vehicle
def __init__(self, make, model, doors):
super().__init__(make, model)
self.doors = doors
# COMPOSITION - "has-a" relationship
class Engine:
def __init__(self, horsepower, fuel_type):
self.horsepower = horsepower
self.fuel_type = fuel_type
def start(self):
return f"Engine starting ({self.horsepower}hp, {self.fuel_type})"
class Car: # Car HAS-A Engine
def __init__(self, make, model, engine):
self.make = make
self.model = model
self.engine = engine # Composition
def start(self):
return f"{self.make} {self.model}: {self.engine.start()}"
# Usage
engine = Engine(200, "Gasoline")
car = Car("Toyota", "Camry", engine)
print(car.start())class DatabaseConnection:
def __init__(self, host, port):
self.host = host
self.port = port
self.connected = False
def connect(self):
self.connected = True
return f"Connected to {self.host}:{self.port}"
def execute(self, query):
if not self.connected:
raise RuntimeError("Not connected to database")
return f"Executing: {query}"
class Repository:
"""Delegates database operations to DatabaseConnection"""
def __init__(self, connection):
self._connection = connection
def find_by_id(self, table, id):
query = f"SELECT * FROM {table} WHERE id = {id}"
return self._connection.execute(query)
def save(self, table, data):
query = f"INSERT INTO {table} VALUES ({data})"
return self._connection.execute(query)
# Delegate connection methods
def connect(self):
return self._connection.connect()
def __getattr__(self, name):
"""Delegate any unknown attributes to the connection"""
return getattr(self._connection, name)
# Usage
db = DatabaseConnection("localhost", 5432)
repo = Repository(db)
repo.connect()
result = repo.find_by_id("users", 1)class SingletonMeta(type):
"""Metaclass that creates singleton instances"""
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Database(metaclass=SingletonMeta):
def __init__(self, connection_string):
self.connection_string = connection_string
self.connected = False
def connect(self):
if not self.connected:
print(f"Connecting to {self.connection_string}")
self.connected = True
class UserDatabase(Database):
def get_users(self):
return ["Alice", "Bob", "Charlie"]
# Both instances are the same object due to singleton metaclass
db1 = UserDatabase("postgresql://localhost/users")
db2 = UserDatabase("mysql://localhost/users") # Same instance!
print(db1 is db2) # True# ❌ Overusing inheritance
class Animal:
def move(self):
pass
class FlyingAnimal(Animal):
def fly(self):
pass
class SwimmingAnimal(Animal):
def swim(self):
pass
class FlyingSwimmingAnimal(FlyingAnimal, SwimmingAnimal): # Complex hierarchy
pass
# ✅ Using composition
class Animal:
def __init__(self, movement_abilities=None):
self.movement_abilities = movement_abilities or []
def move(self):
for ability in self.movement_abilities:
ability.execute()
class FlyingAbility:
def execute(self):
print("Flying through the air")
class SwimmingAbility:
def execute(self):
print("Swimming in water")
# More flexible - can combine abilities easily
duck = Animal([FlyingAbility(), SwimmingAbility()])
duck.move()# ❌ Deep inheritance hierarchy
class A:
pass
class B(A):
pass
class C(B):
pass
class D(C): # Too deep!
pass
# ✅ Shallow hierarchy with composition
class Component:
pass
class Feature:
def __init__(self, components):
self.components = components
class Product:
def __init__(self, features):
self.features = featuresfrom abc import ABC, abstractmethod
# ✅ Good - Clear interface definition
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount, card_info):
pass
@abstractmethod
def refund_payment(self, transaction_id):
pass
class StripeProcessor(PaymentProcessor):
def process_payment(self, amount, card_info):
return f"Processing ${amount} via Stripe"
def refund_payment(self, transaction_id):
return f"Refunding transaction {transaction_id} via Stripe"
class PayPalProcessor(PaymentProcessor):
def process_payment(self, amount, card_info):
return f"Processing ${amount} via PayPal"
def refund_payment(self, transaction_id):
return f"Refunding transaction {transaction_id} via PayPal"class Parent:
def __init__(self, name):
self.name = name
print(f"Parent.__init__({name})")
class Child(Parent):
def __init__(self, name, age):
# ✅ Good - Always use super()
super().__init__(name)
self.age = age
print(f"Child.__init__({name}, {age})")
def greet(self):
# ✅ Good - Extend parent behavior
parent_greeting = super().greet() if hasattr(super(), 'greet') else ""
return f"Child greeting: {parent_greeting}"class Vehicle:
"""
Base class for all vehicles.
Provides common functionality for starting, stopping, and basic operations.
Subclasses should implement vehicle-specific behavior.
"""
def __init__(self, make, model):
self.make = make
self.model = model
def start(self):
"""Start the vehicle. Override in subclasses for specific behavior."""
return f"Starting {self.make} {self.model}"
class Car(Vehicle):
"""
Car implementation of Vehicle.
Extends Vehicle with car-specific functionality like doors and trunk.
Inherits basic vehicle operations from parent class.
"""
def __init__(self, make, model, doors):
super().__init__(make, model)
self.doors = doors
def start(self):
"""Override to add car-specific starting behavior."""
base_start = super().start()
return f"{base_start} - Engine running"# ❌ Problematic diamond inheritance
class A:
def method(self):
print("A")
class B(A):
def method(self):
print("B")
A.method(self) # Direct call - problematic!
class C(A):
def method(self):
print("C")
A.method(self) # Direct call - problematic!
class D(B, C):
def method(self):
print("D")
B.method(self)
C.method(self) # A.method() called twice!
# ✅ Solution using super()
class A:
def method(self):
print("A")
class B(A):
def method(self):
print("B")
super().method() # Cooperative inheritance
class C(A):
def method(self):
print("C")
super().method() # Cooperative inheritance
class D(B, C):
def method(self):
print("D")
super().method() # Follows MRO correctly# ❌ Forgetting to initialize parent
class Parent:
def __init__(self, name):
self.name = name
self.initialized = True
class Child(Parent):
def __init__(self, name, age):
# Missing super().__init__(name)!
self.age = age
child = Child("Alice", 10)
# print(child.name) # AttributeError!
# ✅ Always call parent constructor
class Child(Parent):
def __init__(self, name, age):
super().__init__(name) # Initialize parent
self.age = age# ❌ Breaking parent contract
class BankAccount:
def withdraw(self, amount):
if amount <= self.balance:
self.balance -= amount
return True
return False
class SavingsAccount(BankAccount):
def withdraw(self, amount):
# Breaks parent contract - doesn't return boolean!
if amount <= self.balance:
self.balance -= amount
print("Withdrawal successful")
else:
print("Insufficient funds")
# ✅ Maintain parent contract
class SavingsAccount(BankAccount):
def withdraw(self, amount):
success = super().withdraw(amount)
if success:
print("Withdrawal successful")
else:
print("Insufficient funds")
return success # Maintain return type# ❌ Confusing multiple inheritance
class Printer:
def print(self, document):
print(f"Printing: {document}")
class Scanner:
def scan(self, document):
print(f"Scanning: {document}")
class Copier(Printer, Scanner):
def copy(self, document):
scanned = self.scan(document)
self.print(scanned) # Confusing interaction
# ✅ Clear composition approach
class MultiFunctionDevice:
def __init__(self):
self.printer = Printer()
self.scanner = Scanner()
def copy(self, document):
scanned = self.scanner.scan(document)
self.printer.print(scanned)from abc import ABC, abstractmethod
class Character(ABC):
"""Base character class"""
def __init__(self, name, health, attack_power):
self.name = name
self.max_health = health
self.health = health
self.attack_power = attack_power
self.level = 1
@abstractmethod
def special_ability(self):
"""Each character type has a unique special ability"""
pass
def attack(self, target):
damage = self.attack_power
target.take_damage(damage)
return f"{self.name} attacks {target.name} for {damage} damage!"
def take_damage(self, damage):
self.health = max(0, self.health - damage)
if self.health == 0:
return f"{self.name} has been defeated!"
return f"{self.name} takes {damage} damage! Health: {self.health}"
def heal(self, amount):
self.health = min(self.max_health, self.health + amount)
return f"{self.name} heals for {amount}! Health: {self.health}"
class Warrior(Character):
def __init__(self, name):
super().__init__(name, health=120, attack_power=25)
self.armor = 10
def special_ability(self):
damage_boost = self.attack_power * 2
return f"{self.name} uses Berserker Rage! Next attack deals {damage_boost} damage!"
def take_damage(self, damage):
reduced_damage = max(1, damage - self.armor)
return super().take_damage(reduced_damage)
class Mage(Character):
def __init__(self, name):
super().__init__(name, health=80, attack_power=35)
self.mana = 100
def special_ability(self):
if self.mana >= 30:
self.mana -= 30
return f"{self.name} casts Fireball! Deals massive damage!"
return f"{self.name} doesn't have enough mana!"
def meditate(self):
self.mana = min(100, self.mana + 20)
return f"{self.name} meditates and recovers mana. Mana: {self.mana}"
class Rogue(Character):
def __init__(self, name):
super().__init__(name, health=90, attack_power=30)
self.stealth = False
def special_ability(self):
self.stealth = True
return f"{self.name} enters stealth mode! Next attack is critical!"
def attack(self, target):
if self.stealth:
damage = self.attack_power * 2 # Critical hit
self.stealth = False
target.take_damage(damage)
return f"{self.name} performs a sneak attack on {target.name} for {damage} damage!"
return super().attack(target)
# Usage
warrior = Warrior("Conan")
mage = Mage("Gandalf")
rogue = Rogue("Legolas")
print(warrior.special_ability())
print(mage.special_ability())
print(rogue.special_ability())
print(rogue.attack(warrior))from abc import ABC, abstractmethod
import json
class Document(ABC):
"""Abstract base class for all document types"""
def __init__(self, title, content):
self.title = title
self.content = content
self.metadata = {}
@abstractmethod
def export(self, format_type):
"""Export document in specified format"""
pass
@abstractmethod
def validate(self):
"""Validate document content"""
pass
def add_metadata(self, key, value):
self.metadata[key] = value
def get_word_count(self):
return len(self.content.split())
class TextDocument(Document):
def __init__(self, title, content):
super().__init__(title, content)
self.encoding = "utf-8"
def export(self, format_type):
if format_type == "txt":
return f"Title: {self.title}\n\n{self.content}"
elif format_type == "json":
return json.dumps({
"title": self.title,
"content": self.content,
"metadata": self.metadata
})
else:
raise ValueError(f"Unsupported format: {format_type}")
def validate(self):
return len(self.title) > 0 and len(self.content) > 0
class MarkdownDocument(TextDocument):
def __init__(self, title, content):
super().__init__(title, content)
self.has_toc = False
def export(self, format_type):
if format_type == "md":
return f"# {self.title}\n\n{self.content}"
elif format_type == "html":
return f"<h1>{self.title}</h1>\n<p>{self.content}</p>"
else:
return super().export(format_type) # Delegate to parent
def generate_toc(self):
lines = self.content.split('\n')
toc = []
for line in lines:
if line.startswith('#'):
level = len(line) - len(line.lstrip('#'))
title = line.lstrip('# ')
toc.append(' ' * (level-1) + f"- {title}")
return '\n'.join(toc)
class PDFDocument(Document):
def __init__(self, title, content, page_count):
super().__init__(title, content)
self.page_count = page_count
def export(self, format_type):
if format_type == "pdf":
return f"PDF Document: {self.title} ({self.page_count} pages)"
elif format_type == "txt":
return f"Title: {self.title}\nPages: {self.page_count}\n\n{self.content}"
else:
raise ValueError(f"PDF cannot be exported to {format_type}")
def validate(self):
return (super().validate() and
self.page_count > 0 and
len(self.content) > self.page_count * 100) # Rough estimate
# Usage
text_doc = TextDocument("My Essay", "This is a sample essay content...")
md_doc = MarkdownDocument("README", "# Introduction\nThis is a markdown document.")
pdf_doc = PDFDocument("Research Paper", "Abstract: This paper discusses...", 10)
# Polymorphism - treat all documents the same way
documents = [text_doc, md_doc, pdf_doc]
for doc in documents:
print(f"Document: {doc.title}")
print(f"Valid: {doc.validate()}")
print(f"Word count: {doc.get_word_count()}")
print("---")-
Animal Hierarchy: Create an
Animalbase class withDog,Cat, andBirdsubclasses. -
Shape Calculator: Build a
Shapehierarchy withCircle,Rectangle, andTriangleclasses. -
Employee System: Create
Employeebase class withManagerandDevelopersubclasses. -
Vehicle Fleet: Design a vehicle system with
Car,Truck, andMotorcycleclasses. -
Bank Accounts: Implement
BankAccountwithSavingsAccountandCheckingAccountsubclasses.
-
Abstract Media Player: Create an abstract
MediaPlayerwithAudioPlayerandVideoPlayerimplementations. -
Game Items: Design an RPG item system with weapons, armor, and consumables.
-
File System: Build a file system hierarchy with files, directories, and symbolic links.
-
Database Adapters: Create database connection adapters for different database types.
-
UI Components: Design a UI component hierarchy with buttons, text fields, and containers.
-
Plugin Architecture: Implement a plugin system using abstract base classes and dynamic loading.
-
State Machine: Create a state machine framework using inheritance and polymorphism.
-
Template Method Pattern: Implement a data processing pipeline using the template method pattern.
-
Multiple Inheritance Design: Design a complex system using multiple inheritance and mixins.
-
Metaclass Inheritance: Create a custom metaclass that affects inheritance behavior.
Congratulations! You've completed the comprehensive guide to Python Inheritance. Here's what you've mastered:
- ✅ Basic Inheritance: Creating parent-child class relationships
- ✅ Method Overriding: Customizing inherited behavior
- ✅ super() Function: Proper parent method calling
- ✅ Multiple Inheritance: Combining functionality from multiple sources
- ✅ Abstract Base Classes: Defining contracts and interfaces
- ✅ Method Resolution Order: Understanding Python's inheritance algorithm
- ✅ Best Practices: When and how to use inheritance effectively
- Liskov Substitution Principle: Subclasses should be substitutable for their base classes
- Open/Closed Principle: Classes should be open for extension, closed for modification
- Composition over Inheritance: Prefer "has-a" relationships when appropriate
- Interface Segregation: Use abstract base classes to define clear contracts
- Practice: Work through the exercises to reinforce your learning
- Study Polymorphism: Learn about duck typing and method overloading
- Explore Design Patterns: Study common patterns that use inheritance
- Read Real Code: Examine how popular Python libraries use inheritance
- Build Projects: Apply inheritance concepts to real-world problems
- Inheritance models "is-a" relationships - use it when it makes logical sense
- Composition is often better - prefer "has-a" relationships when possible
- Keep hierarchies shallow - deep inheritance trees are hard to maintain
- Use abstract base classes - define clear interfaces and contracts
- Always call super() - ensure proper initialization and method chaining
Happy coding! 🐍✨
This tutorial is part of the Python Practices repository. Continue your OOP journey with the Polymorphism and Design Patterns tutorials.