Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

README.md

Python Inheritance: Complete Tutorial 🧬

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.

📚 Table of Contents

  1. Introduction to Inheritance
  2. What is Inheritance?
  3. Basic Inheritance Syntax
  4. The super() Function
  5. Method Overriding
  6. Types of Inheritance
  7. Abstract Base Classes
  8. Multiple Inheritance
  9. Method Resolution Order (MRO)
  10. Advanced Inheritance Concepts
  11. Best Practices
  12. Common Pitfalls and Solutions
  13. Real-World Examples
  14. Exercises and Practice

Introduction to Inheritance

What is Inheritance?

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.)

Why Use Inheritance?

  1. Code Reusability: Write common functionality once in a parent class
  2. Hierarchical Organization: Model real-world relationships naturally
  3. Polymorphism: Treat different objects uniformly through a common interface
  4. Extensibility: Add new functionality while preserving existing behavior
  5. Maintainability: Changes to common functionality affect all subclasses

Key Terminology

  • 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

What is Inheritance?

The "Is-A" Relationship

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
    pass

Inheritance vs Composition

Inheritance ("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

Visual Hierarchy Example

# 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

Basic Inheritance Syntax

Simple Inheritance

# 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

Checking Inheritance Relationships

# 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

Understanding super()

The super() function provides access to methods in a parent class from a child class. It's essential for proper inheritance implementation.

Basic super() Usage

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 method

Different Ways to Use super()

1. Calling Parent Constructor

class 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 = 0

2. Extending Parent Methods

class 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.balance

3. Cooperative Inheritance (Advanced)

class 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.method

super() vs Direct Parent Call

class 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 changes

Method Overriding

What is Method Overriding?

Method overriding allows a child class to provide a specific implementation of a method that is already defined in its parent class.

Basic Method Overriding

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())

Overriding with Extension

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()}")

Overriding Special Methods

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)

Types of Inheritance

1. Single Inheritance

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 method

2. Multiple Inheritance

One 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 method

3. Multilevel Inheritance

A 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 method

4. Hierarchical Inheritance

Multiple 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 Designer

5. Hybrid Inheritance

Combination 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 method

Abstract Base Classes

What are Abstract Base Classes?

Abstract 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.

Using the abc Module

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()}")

Abstract Properties

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())

Template Method Pattern with ABC

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

Understanding Multiple Inheritance

Multiple inheritance allows a class to inherit from more than one parent class, combining functionality from multiple sources.

Basic Multiple Inheritance

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 Flyer

Diamond Problem and MRO

The 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.method

Practical Multiple Inheritance Example

class 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 method

Mixin Classes

Mixins 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 (MRO)

Understanding MRO

Method Resolution Order determines the order in which Python searches for methods in inheritance hierarchies, especially important with multiple inheritance.

C3 Linearization Algorithm

Python uses the C3 linearization algorithm to determine MRO, which ensures:

  1. Children are checked before parents
  2. Parents are checked in the order they appear in the class definition
  3. 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)

Complex MRO Example

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 walks

Cooperative Inheritance with super()

class 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")

Advanced Inheritance Concepts

Composition vs Inheritance

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())

Delegation Pattern

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)

Metaclasses and Inheritance

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

Best Practices

1. Favor Composition Over Inheritance

# ❌ 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()

2. Keep Inheritance Hierarchies Shallow

# ❌ 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 = features

3. Use Abstract Base Classes for Interfaces

from 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"

4. Use super() Consistently

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}"

5. Document Inheritance Relationships

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"

Common Pitfalls and Solutions

1. The Diamond Problem

# ❌ 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

2. Forgetting to Call super().init()

# ❌ 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

3. Overriding Methods Incorrectly

# ❌ 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

4. Misusing Multiple Inheritance

# ❌ 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)

Real-World Examples

Example 1: Game Character System

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))

Example 2: Document Processing System

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("---")

Exercises and Practice

Beginner Level 🟢

  1. Animal Hierarchy: Create an Animal base class with Dog, Cat, and Bird subclasses.

  2. Shape Calculator: Build a Shape hierarchy with Circle, Rectangle, and Triangle classes.

  3. Employee System: Create Employee base class with Manager and Developer subclasses.

  4. Vehicle Fleet: Design a vehicle system with Car, Truck, and Motorcycle classes.

  5. Bank Accounts: Implement BankAccount with SavingsAccount and CheckingAccount subclasses.

Intermediate Level 🟡

  1. Abstract Media Player: Create an abstract MediaPlayer with AudioPlayer and VideoPlayer implementations.

  2. Game Items: Design an RPG item system with weapons, armor, and consumables.

  3. File System: Build a file system hierarchy with files, directories, and symbolic links.

  4. Database Adapters: Create database connection adapters for different database types.

  5. UI Components: Design a UI component hierarchy with buttons, text fields, and containers.

Advanced Level 🔴

  1. Plugin Architecture: Implement a plugin system using abstract base classes and dynamic loading.

  2. State Machine: Create a state machine framework using inheritance and polymorphism.

  3. Template Method Pattern: Implement a data processing pipeline using the template method pattern.

  4. Multiple Inheritance Design: Design a complex system using multiple inheritance and mixins.

  5. Metaclass Inheritance: Create a custom metaclass that affects inheritance behavior.


Summary

Congratulations! You've completed the comprehensive guide to Python Inheritance. Here's what you've mastered:

Key Concepts Learned:

  • 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

Design Principles Mastered:

  • 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

Next Steps:

  1. Practice: Work through the exercises to reinforce your learning
  2. Study Polymorphism: Learn about duck typing and method overloading
  3. Explore Design Patterns: Study common patterns that use inheritance
  4. Read Real Code: Examine how popular Python libraries use inheritance
  5. Build Projects: Apply inheritance concepts to real-world problems

Remember:

  • 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.