🧬 Class Inheritance

Class inheritance allows you to create new classes that build upon existing ones, inheriting their attributes and methods while adding specialized functionality. Instead of writing everything from scratch, inheritance lets you extend and customize existing code, creating hierarchies of related classes that share common features.

Think of inheritance like family traits - children inherit characteristics from their parents but also develop their own unique features. In programming, a child class inherits from a parent class but can add new methods or modify existing ones.

# Basic inheritance example
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} makes a sound"
    
    def sleep(self):
        return f"{self.name} is sleeping"

class Dog(Animal):  # Dog inherits from Animal
    def speak(self):  # Override parent method
        return f"{self.name} barks!"
    
    def fetch(self):  # Add new method
        return f"{self.name} fetches the ball"

# Using inheritance
my_dog = Dog("Buddy")
print(my_dog.speak())   # Uses overridden method
print(my_dog.sleep())   # Uses inherited method
print(my_dog.fetch())   # Uses new method

🎯 Understanding Inheritance

Inheritance creates an "is-a" relationship between classes, where a child class is a specialized version of its parent class. The child class automatically gets all the functionality of the parent class and can extend or modify it as needed.

Creating Child Classes

To create a child class, specify the parent class in parentheses after the child class name. The child automatically inherits all parent attributes and methods.

class Vehicle:
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model
        self.fuel = 100
    
    def start(self):
        return f"{self.brand} {self.model} is starting"
    
    def stop(self):
        return f"{self.brand} {self.model} has stopped"
    
    def get_info(self):
        return f"{self.brand} {self.model} (Fuel: {self.fuel}%)"

class Car(Vehicle):  # Car inherits from Vehicle
    def __init__(self, brand, model, doors):
        super().__init__(brand, model)  # Call parent constructor
        self.doors = doors  # Add car-specific attribute
    
    def honk(self):  # Add car-specific method
        return f"{self.brand} {self.model} honks the horn!"

# Using the child class
my_car = Car("Toyota", "Camry", 4)
print(my_car.start())     # Inherited method
print(my_car.honk())      # New method
print(my_car.get_info())  # Inherited method
print(f"Doors: {my_car.doors}")  # New attribute

The super() Function

The super() function provides access to the parent class, allowing you to call parent methods from child classes. This is especially useful in constructors and when extending parent functionality.

class Employee:
    def __init__(self, name, employee_id):
        self.name = name
        self.employee_id = employee_id
        self.salary = 0
    
    def work(self):
        return f"{self.name} is working"
    
    def get_details(self):
        return f"Employee {self.employee_id}: {self.name} (${self.salary})"

class Manager(Employee):
    def __init__(self, name, employee_id, department):
        super().__init__(name, employee_id)  # Initialize parent
        self.department = department
        self.team_size = 0
    
    def work(self):
        parent_work = super().work()  # Get parent behavior
        return f"{parent_work} and managing {self.department}"
    
    def hire(self, employee):
        self.team_size += 1
        return f"{self.name} hired {employee} to {self.department}"

# Using super() functionality
manager = Manager("Alice", "M001", "Engineering")
manager.salary = 80000
print(manager.work())        # Extended behavior
print(manager.get_details()) # Inherited method
print(manager.hire("Bob"))   # New method

⚡ Method Overriding

Method overriding allows child classes to provide specialized implementations of parent methods. The child class method replaces the parent version, enabling customized behavior while maintaining the same interface.

Overriding Parent Methods

When a child class defines a method with the same name as a parent method, the child version takes precedence. This allows specialized behavior while keeping the same method names and calling patterns.

class Shape:
    def __init__(self, color):
        self.color = color
    
    def area(self):
        return 0  # Default implementation
    
    def describe(self):
        return f"A {self.color} shape with area {self.area()}"

class Rectangle(Shape):
    def __init__(self, color, width, height):
        super().__init__(color)
        self.width = width
        self.height = height
    
    def area(self):  # Override parent method
        return self.width * self.height

class Circle(Shape):
    def __init__(self, color, radius):
        super().__init__(color)
        self.radius = radius
    
    def area(self):  # Override parent method
        return 3.14 * self.radius * self.radius

# Each shape calculates area differently
rect = Rectangle("blue", 5, 3)
circle = Circle("red", 4)

print(rect.describe())   # Uses Rectangle's area method
print(circle.describe()) # Uses Circle's area method

Extending Parent Methods

Sometimes you want to add to parent functionality rather than replace it completely. Using super() allows you to call the parent method and then add additional behavior.

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance
        self.transactions = []
    
    def deposit(self, amount):
        self.balance += amount
        self.transactions.append(f"Deposited ${amount}")
        return f"Deposited ${amount}. Balance: ${self.balance}"

class SavingsAccount(BankAccount):
    def __init__(self, owner, balance=0, interest_rate=0.02):
        super().__init__(owner, balance)
        self.interest_rate = interest_rate
    
    def deposit(self, amount):
        # Extend parent functionality
        result = super().deposit(amount)  # Call parent method
        if amount >= 1000:  # Add bonus for large deposits
            bonus = amount * 0.01
            self.balance += bonus
            self.transactions.append(f"Bonus: ${bonus}")
            result += f" + ${bonus} bonus!"
        return result
    
    def add_interest(self):
        interest = self.balance * self.interest_rate
        self.balance += interest
        self.transactions.append(f"Interest: ${interest}")
        return f"Added ${interest} interest"

# Extended functionality in action
savings = SavingsAccount("Alice", 500, 0.03)
print(savings.deposit(1500))  # Gets bonus
print(savings.add_interest()) # New method

🚀 Multiple Inheritance

Python supports multiple inheritance, where a class can inherit from multiple parent classes. This powerful feature allows combining functionality from different sources but requires careful design to avoid conflicts.

Combining Multiple Parents

Multiple inheritance allows a class to inherit from several parent classes, gaining functionality from all of them.

class Flyable:
    def fly(self):
        return f"{self.name} is flying"
    
    def land(self):
        return f"{self.name} has landed"

class Swimmable:
    def swim(self):
        return f"{self.name} is swimming"
    
    def dive(self):
        return f"{self.name} is diving"

class Duck(Flyable, Swimmable):  # Multiple inheritance
    def __init__(self, name):
        self.name = name
    
    def quack(self):
        return f"{self.name} says quack!"

# Duck can fly and swim
donald = Duck("Donald")
print(donald.quack())  # Own method
print(donald.fly())    # From Flyable
print(donald.swim())   # From Swimmable
print(donald.land())   # From Flyable
print(donald.dive())   # From Swimmable

Method Resolution Order (MRO)

When multiple parent classes have methods with the same name, Python uses Method Resolution Order (MRO) to determine which method to call.

class A:
    def greet(self):
        return "Hello from A"

class B:
    def greet(self):
        return "Hello from B"

class C(A, B):  # Inherits from both A and B
    pass

class D(B, A):  # Different order
    pass

# MRO determines which method is called
c = C()
d = D()

print(c.greet())  # Uses A's method (A comes first)
print(d.greet())  # Uses B's method (B comes first)

# Check the method resolution order
print(f"C's MRO: {[cls.__name__ for cls in C.__mro__]}")
print(f"D's MRO: {[cls.__name__ for cls in D.__mro__]}")

🌟 Practical Applications

Building Class Hierarchies

Inheritance enables creating logical hierarchies that model real-world relationships.

class User:
    def __init__(self, username, email):
        self.username = username
        self.email = email
        self.active = True
    
    def login(self):
        return f"{self.username} logged in"
    
    def logout(self):
        return f"{self.username} logged out"

class Customer(User):
    def __init__(self, username, email):
        super().__init__(username, email)
        self.orders = []
        self.loyalty_points = 0
    
    def place_order(self, order):
        self.orders.append(order)
        self.loyalty_points += 10
        return f"Order placed by {self.username}"

class Admin(User):
    def __init__(self, username, email, permissions):
        super().__init__(username, email)
        self.permissions = permissions
    
    def manage_users(self, action, user):
        if "user_management" in self.permissions:
            return f"{self.username} performed {action} on {user}"
        return "Permission denied"

# Different user types with specialized behavior
customer = Customer("alice", "alice@email.com")
admin = Admin("admin", "admin@email.com", ["user_management", "system_admin"])

print(customer.login())
print(customer.place_order("laptop"))
print(admin.manage_users("activate", "bob"))

💡 Design Patterns with Inheritance

Template Method Pattern

Use inheritance to define a skeleton algorithm while allowing subclasses to override specific steps.

class DataProcessor:
    def process(self, data):
        # Template method defining the algorithm
        cleaned = self.clean_data(data)
        processed = self.transform_data(cleaned)
        result = self.save_data(processed)
        return result
    
    def clean_data(self, data):
        # Default implementation
        return [item.strip() for item in data if item.strip()]
    
    def transform_data(self, data):
        # Must be implemented by subclasses
        raise NotImplementedError("Subclasses must implement transform_data")
    
    def save_data(self, data):
        # Default implementation
        return f"Saved {len(data)} items"

class NumberProcessor(DataProcessor):
    def transform_data(self, data):
        return [float(item) for item in data if item.replace('.', '').isdigit()]

class TextProcessor(DataProcessor):
    def transform_data(self, data):
        return [item.upper() for item in data]

# Template pattern in action
number_processor = NumberProcessor()
text_processor = TextProcessor()

numbers = ["1.5", "2.0", "invalid", "3.7"]
texts = ["hello", "world", "python"]

print(number_processor.process(numbers))
print(text_processor.process(texts))

Hands-on Exercise

Create a simple Animal inheritance system. Make a base Animal class with name and age attributes, and a speak() method. Then create Dog and Cat classes that inherit from Animal and override the speak() method with their specific sounds.

python
class Animal:
    def __init__(self, name, age):
        # TODO: Set name and age attributes
        pass
    
    def speak(self):
        # TODO: Generic animal sound
        pass
    
    def get_info(self):
        # TODO: Return animal information
        pass

class Dog(Animal):
    def __init__(self, name, age):
        # TODO: Call parent constructor
        pass
    
    def speak(self):
        # TODO: Override with dog sound
        pass

class Cat(Animal):
    def __init__(self, name, age):
        # TODO: Call parent constructor
        pass
    
    def speak(self):
        # TODO: Override with cat sound
        pass

# TODO: Test your classes
dog = Dog("Buddy", 5)
cat = Cat("Whiskers", 3)

print(dog.get_info())
print(dog.speak())
print(cat.get_info())
print(cat.speak())

Solution and Explanation 💡

Click to see the complete solution
class Animal:
    def __init__(self, name, age):
        # Set name and age attributes
        self.name = name
        self.age = age
    
    def speak(self):
        # Generic animal sound
        return f"{self.name} makes a sound"
    
    def get_info(self):
        # Return animal information
        return f"{self.name} is {self.age} years old"

class Dog(Animal):
    def __init__(self, name, age):
        # Call parent constructor
        super().__init__(name, age)
    
    def speak(self):
        # Override with dog sound
        return f"{self.name} says Woof!"

class Cat(Animal):
    def __init__(self, name, age):
        # Call parent constructor
        super().__init__(name, age)
    
    def speak(self):
        # Override with cat sound
        return f"{self.name} says Meow!"

# Test your classes
dog = Dog("Buddy", 5)
cat = Cat("Whiskers", 3)

print(dog.get_info())
print(dog.speak())
print(cat.get_info())
print(cat.speak())

Key Learning Points:

  • 📌 Inheritance: Child classes inherit attributes and methods from parent class
  • 📌 super(): Use super().__init__() to call parent constructor
  • 📌 Method overriding: Child classes can provide their own version of parent methods
  • 📌 Code reuse: Common functionality stays in parent class, specific behavior in child classes
  • 📌 Polymorphism: Different objects can respond to same method call differently

Test Your Knowledge

Test what you've learned about class inheritance:

What's Next?

Now that you understand inheritance basics, you're ready to dive deeper into method overriding. This technique allows you to customize inherited methods to create specialized behaviors while maintaining the same interface.

Ready to continue? Check out our lesson on Method Overriding.

Was this helpful?

😔Poor
🙁Fair
😊Good
😄Great
🤩Excellent