Python Programming OOP Inheritance
4 Pillars of OOP Multiple Inheritance

Python Inheritance Complete Guide

Learn Python inheritance - single, multiple, multilevel inheritance, method overriding, polymorphism, MRO, abstract classes, and OOP best practices with practical examples.

Inheritance

Parent-child relationships

Polymorphism

Many forms, one interface

Encapsulation

Data hiding

Abstraction

Hide complexity

What is Inheritance in Python?

Inheritance is a fundamental concept in Object-Oriented Programming (OOP) that allows a class (child/derived class) to inherit properties and methods from another class (parent/base class). It promotes code reusability and establishes relationships between classes.

Key Concept

Inheritance creates a parent-child relationship between classes. The child class inherits all attributes and methods of the parent class and can add its own specific features. Python supports multiple inheritance (a class can inherit from multiple parent classes).

Basic Inheritance Example
# Basic Inheritance Examples

print("=== Basic Inheritance ===")

# Parent class (Base class)
class Animal:
    def __init__(self, name, species):
        self.name = name
        self.species = species
    
    def make_sound(self):
        return "Some generic animal sound"
    
    def info(self):
        return f"{self.name} is a {self.species}"

# Child class (Derived class) - Single Inheritance
class Dog(Animal):
    def __init__(self, name, breed):
        # Call parent class constructor
        super().__init__(name, species="Dog")
        self.breed = breed
    
    # Method overriding
    def make_sound(self):
        return "Woof! Woof!"
    
    # Additional method specific to Dog
    def wag_tail(self):
        return f"{self.name} is wagging its tail"

# Child class (Derived class) - Another example
class Cat(Animal):
    def __init__(self, name, color):
        super().__init__(name, species="Cat")
        self.color = color
    
    def make_sound(self):
        return "Meow! Meow!"
    
    def climb_tree(self):
        return f"{self.name} is climbing a tree"

# Using the classes
print("Creating animals:")
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers", "Orange")

print(dog.info())
print(f"Sound: {dog.make_sound()}")
print(f"Breed: {dog.breed}")
print(dog.wag_tail())

print(f"\n{cat.info()}")
print(f"Sound: {cat.make_sound()}")
print(f"Color: {cat.color}")
print(cat.climb_tree())

# Checking inheritance
print(f"\nIs Dog a subclass of Animal? {issubclass(Dog, Animal)}")
print(f"Is dog an instance of Animal? {isinstance(dog, Animal)}")
print(f"Is dog an instance of Dog? {isinstance(dog, Dog)}")

Types of Inheritance in Python

Python supports various types of inheritance. Understanding these patterns helps design better class hierarchies and relationships.

Complete Inheritance Types Reference Table

Inheritance Type Description Syntax Example When to Use Advantages
Single Inheritance One child class inherits from one parent class class Child(Parent): Simple parent-child relationship Simple, clear hierarchy
Multiple Inheritance One child class inherits from multiple parent classes class Child(Parent1, Parent2): Combining features from multiple sources Code reuse from multiple classes
Multilevel Inheritance Chain of inheritance (grandparent → parent → child) class GrandChild(Child): Building specialized classes gradually Progressive specialization
Hierarchical Inheritance Multiple children inherit from one parent class Child1(Parent): class Child2(Parent): Shared base with different specializations Shared functionality with variations
Hybrid Inheritance Combination of multiple inheritance types Complex combinations Complex real-world relationships Flexibility in modeling
Inheritance Hierarchy Visualization
Vehicle (Base)
Car
SportsCar
Bike
Truck

Inheritance Examples with Output

Let's explore practical examples of each inheritance type with actual Python code and output.

Single and Multilevel Inheritance
# Single and Multilevel Inheritance Examples

print("=== Single and Multilevel Inheritance ===")

# Base class
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def display_info(self):
        return f"Name: {self.name}, Age: {self.age}"
    
    def greet(self):
        return f"Hello, I'm {self.name}"

# Single Inheritance - Employee inherits from Person
class Employee(Person):
    def __init__(self, name, age, employee_id, department):
        super().__init__(name, age)
        self.employee_id = employee_id
        self.department = department
        self.salary = 0
    
    # Method overriding
    def display_info(self):
        base_info = super().display_info()
        return f"{base_info}, ID: {self.employee_id}, Dept: {self.department}"
    
    # Additional method
    def calculate_bonus(self):
        return self.salary * 0.1

# Multilevel Inheritance - Manager inherits from Employee
class Manager(Employee):
    def __init__(self, name, age, employee_id, department, team_size):
        super().__init__(name, age, employee_id, department)
        self.team_size = team_size
        self.salary = 80000  # Default salary for manager
    
    # Further specialization
    def display_info(self):
        employee_info = super().display_info()
        return f"{employee_info}, Team Size: {self.team_size}, Salary: ${self.salary:,.2f}"
    
    def conduct_meeting(self):
        return f"Manager {self.name} is conducting a meeting with {self.team_size} team members"

# Using the classes
print("Creating instances:")
person = Person("Alice", 30)
employee = Employee("Bob", 25, "EMP001", "Engineering")
manager = Manager("Charlie", 40, "MGR001", "Engineering", 10)

print("\nPerson:")
print(person.display_info())
print(person.greet())

print("\nEmployee:")
print(employee.display_info())
employee.salary = 60000
print(f"Bonus: ${employee.calculate_bonus():,.2f}")

print("\nManager:")
print(manager.display_info())
print(f"Bonus: ${manager.calculate_bonus():,.2f}")
print(manager.conduct_meeting())

# Check inheritance chain
print("\nInheritance checks:")
print(f"Is Manager subclass of Employee? {issubclass(Manager, Employee)}")
print(f"Is Manager subclass of Person? {issubclass(Manager, Person)}")
print(f"Is Employee subclass of Person? {issubclass(Employee, Person)}")
print(f"Is Person subclass of Employee? {issubclass(Person, Employee)}")

# Method Resolution Order (MRO)
print("\nMethod Resolution Order for Manager:")
print(Manager.__mro__)
print("\nMethod Resolution Order for Employee:")
print(Employee.__mro__)
Multiple Inheritance and MRO
# Multiple Inheritance and MRO Examples

print("=== Multiple Inheritance and MRO ===")

# Parent class 1
class Flyable:
    def __init__(self, max_altitude):
        self.max_altitude = max_altitude
    
    def fly(self):
        return f"Flying at altitude {self.max_altitude} feet"
    
    def land(self):
        return "Landing safely"
    
    def common_method(self):
        return "Method from Flyable class"

# Parent class 2
class Swimmable:
    def __init__(self, max_depth):
        self.max_depth = max_depth
    
    def swim(self):
        return f"Swimming at depth {self.max_depth} meters"
    
    def dive(self):
        return "Diving underwater"
    
    def common_method(self):
        return "Method from Swimmable class"

# Child class with multiple inheritance
class AmphibiousVehicle(Flyable, Swimmable):
    def __init__(self, name, max_altitude, max_depth):
        # Initialize both parent classes
        Flyable.__init__(self, max_altitude)
        Swimmable.__init__(self, max_depth)
        self.name = name
    
    def operate(self):
        return f"{self.name} can both fly and swim!"
    
    # Override common_method to resolve ambiguity
    def common_method(self):
        # Call specific parent's method or provide new implementation
        return "AmphibiousVehicle's implementation"

# Another example with different MRO
class Vehicle:
    def move(self):
        return "Vehicle is moving"
    
    def common(self):
        return "From Vehicle"

class Car(Vehicle):
    def move(self):
        return "Car is driving on road"
    
    def common(self):
        return "From Car"

class Boat(Vehicle):
    def move(self):
        return "Boat is sailing on water"
    
    def common(self):
        return "From Boat"

class AmphibiousCar(Car, Boat):
    def move(self):
        # You can call parent methods specifically
        car_move = Car.move(self)
        boat_move = Boat.move(self)
        return f"Amphibious Car: {car_move} and {boat_move}"

# Using the classes
print("Creating amphibious vehicle:")
amphibious = AmphibiousVehicle("Hydra", 10000, 200)

print(f"Name: {amphibious.name}")
print(amphibious.fly())
print(amphibious.swim())
print(amphibious.operate())
print(f"Common method: {amphibious.common_method()}")

print("\nCreating amphibious car:")
amphibious_car = AmphibiousCar()
print(amphibious_car.move())
print(f"Common method: {amphibious_car.common()}")

# MRO Demonstration
print("\n=== MRO Demonstration ===")
print("AmphibiousVehicle MRO:")
for i, cls in enumerate(AmphibiousVehicle.__mro__):
    print(f"  {i}: {cls.__name__}")

print("\nAmphibiousCar MRO:")
for i, cls in enumerate(AmphibiousCar.__mro__):
    print(f"  {i}: {cls.__name__}")

# Diamond Problem Example
print("\n=== Diamond Problem Example ===")
class A:
    def method(self):
        return "Method from A"

class B(A):
    def method(self):
        return "Method from B"

class C(A):
    def method(self):
        return "Method from C"

class D(B, C):
    pass  # No method defined, will use MRO to decide

d = D()
print(f"D's method: {d.method()}")
print("D's MRO:", [cls.__name__ for cls in D.__mro__])
Method Overriding and super() Function
# Method Overriding and super() Function Examples

print("=== Method Overriding and super() ===")

# Base class
class Shape:
    def __init__(self, color):
        self.color = color
        self.area = 0
    
    def calculate_area(self):
        return "Area calculation not defined for generic shape"
    
    def display_info(self):
        return f"Shape color: {self.color}, Area: {self.area}"
    
    def describe(self):
        return "I am a shape"

# Child class - Circle
class Circle(Shape):
    def __init__(self, color, radius):
        super().__init__(color)  # Call parent constructor
        self.radius = radius
        self.area = self.calculate_area()  # Calculate area during initialization
    
    # Method overriding
    def calculate_area(self):
        import math
        return math.pi * self.radius ** 2
    
    # Override display_info to add radius
    def display_info(self):
        base_info = super().display_info()  # Call parent method
        return f"{base_info}, Radius: {self.radius}"
    
    # Additional method
    def circumference(self):
        import math
        return 2 * math.pi * self.radius

# Child class - Rectangle
class Rectangle(Shape):
    def __init__(self, color, width, height):
        super().__init__(color)
        self.width = width
        self.height = height
        self.area = self.calculate_area()
    
    def calculate_area(self):
        return self.width * self.height
    
    def display_info(self):
        return f"{super().display_info()}, Width: {self.width}, Height: {self.height}"
    
    def perimeter(self):
        return 2 * (self.width + self.height)

# Child class - Square (inherits from Rectangle)
class Square(Rectangle):
    def __init__(self, color, side):
        # Square is a special case of Rectangle
        super().__init__(color, side, side)
    
    # Override to provide more specific description
    def describe(self):
        return "I am a square (special rectangle with equal sides)"
    
    # Don't override calculate_area - uses Rectangle's method

# Using the classes
print("Creating shapes:")
circle = Circle("Red", 5)
rectangle = Rectangle("Blue", 4, 6)
square = Square("Green", 5)

print("\nCircle:")
print(circle.display_info())
print(f"Circumference: {circle.circumference():.2f}")
print(circle.describe())

print("\nRectangle:")
print(rectangle.display_info())
print(f"Perimeter: {rectangle.perimeter()}")
print(rectangle.describe())

print("\nSquare:")
print(square.display_info())
print(f"Perimeter: {square.perimeter()}")
print(square.describe())

# Demonstrating polymorphism
print("\n=== Polymorphism Demonstration ===")
shapes = [circle, rectangle, square]

for shape in shapes:
    print(f"\n{shape.__class__.__name__}:")
    print(f"  Area: {shape.area:.2f}")
    print(f"  Info: {shape.display_info()}")
    print(f"  Description: {shape.describe()}")

# Using super() in complex hierarchies
print("\n=== Complex super() Usage ===")
class Base:
    def __init__(self):
        print("Base.__init__")
        self.base_value = "base"

class Middle1(Base):
    def __init__(self):
        print("Middle1.__init__")
        super().__init__()
        self.middle1_value = "middle1"

class Middle2(Base):
    def __init__(self):
        print("Middle2.__init__")
        super().__init__()
        self.middle2_value = "middle2"

class Final(Middle1, Middle2):
    def __init__(self):
        print("Final.__init__")
        super().__init__()
        self.final_value = "final"

f = Final()
print(f"\nFinal MRO: {[cls.__name__ for cls in Final.__mro__]}")
print(f"Attributes: base_value={f.base_value}, middle1_value={f.middle1_value}, "
      f"middle2_value={f.middle2_value}, final_value={f.final_value}")

Polymorphism in Python

Polymorphism allows objects of different classes to be treated as objects of a common superclass. It enables methods to do different things based on the object it is acting upon.

Polymorphism Examples
# Polymorphism in Python

print("=== Polymorphism Examples ===")

# Base class with abstract-like method
class Animal:
    def speak(self):
        raise NotImplementedError("Subclass must implement abstract method")

# Different animal classes
class Dog(Animal):
    def speak(self):
        return "Woof!"
    
    def fetch(self):
        return "Fetching the ball"

class Cat(Animal):
    def speak(self):
        return "Meow!"
    
    def climb(self):
        return "Climbing the tree"

class Bird(Animal):
    def speak(self):
        return "Chirp!"
    
    def fly(self):
        return "Flying high"

class Cow(Animal):
    def speak(self):
        return "Moo!"
    
    def give_milk(self):
        return "Giving milk"

# Function demonstrating polymorphism
def animal_sounds(animals):
    for animal in animals:
        print(f"{animal.__class__.__name__} says: {animal.speak()}")

# Using polymorphism
print("Animal sounds:")
animals = [Dog(), Cat(), Bird(), Cow()]
animal_sounds(animals)

# Duck Typing - Python's dynamic polymorphism
print("\n=== Duck Typing ===")
class Car:
    def drive(self):
        return "Car is driving"

class Boat:
    def drive(self):
        return "Boat is sailing"

class Plane:
    def drive(self):
        return "Plane is flying"

# Function that works with any object having drive() method
def travel(vehicle):
    print(vehicle.drive())

print("Different vehicles traveling:")
travel(Car())
travel(Boat())
travel(Plane())

# Operator Overloading
print("\n=== Operator Overloading ===")
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    # Overload + operator
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    # Overload - operator
    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)
    
    # Overload * operator (scalar multiplication)
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)
    
    # Overload string representation
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
    
    # Overload equality operator
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

print("Vector operations:")
v1 = Vector(2, 3)
v2 = Vector(4, 5)

print(f"v1 = {v1}")
print(f"v2 = {v2}")
print(f"v1 + v2 = {v1 + v2}")
print(f"v2 - v1 = {v2 - v1}")
print(f"v1 * 3 = {v1 * 3}")
print(f"v1 == Vector(2, 3): {v1 == Vector(2, 3)}")
print(f"v1 == v2: {v1 == v2}")

# Method Overloading (simulated)
print("\n=== Method Overloading (Simulated) ===")
class Calculator:
    def add(self, *args):
        if len(args) == 2:
            return args[0] + args[1]
        elif len(args) == 3:
            return args[0] + args[1] + args[2]
        else:
            return sum(args)

calc = Calculator()
print(f"add(5, 10) = {calc.add(5, 10)}")
print(f"add(1, 2, 3) = {calc.add(1, 2, 3)}")
print(f"add(1, 2, 3, 4, 5) = {calc.add(1, 2, 3, 4, 5)}")

Abstract Classes and Interfaces

Abstract classes define a common interface for subclasses but cannot be instantiated themselves. They're created using the abc module.

Abstract Classes

Define common interface, cannot be instantiated:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass
    
    @abstractmethod
    def perimeter(self):
        pass
    
    def describe(self):
        return "I am a shape"

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return 3.14 * self.radius ** 2
    
    def perimeter(self):
        return 2 * 3.14 * self.radius

# shape = Shape()  # Error!
circle = Circle(5)
print(circle.area())
Interfaces (Python style)

Using abstract classes as interfaces:

from abc import ABC, abstractmethod

class Flyable(ABC):
    @abstractmethod
    def fly(self):
        pass
    
    @abstractmethod
    def land(self):
        pass

class Swimmable(ABC):
    @abstractmethod
    def swim(self):
        pass
    
    @abstractmethod
    def dive(self):
        pass

class Duck(Flyable, Swimmable):
    def fly(self):
        return "Duck flying"
    
    def land(self):
        return "Duck landing"
    
    def swim(self):
        return "Duck swimming"
    
    def dive(self):
        return "Duck diving"

duck = Duck()
print(duck.fly())
print(duck.swim())

Method Resolution Order (MRO) and super()

MRO determines the order in which base classes are searched when looking for a method. The super() function delegates method calls to the next class in the MRO.

MRO and super() in Depth
# MRO and super() in Depth

print("=== Understanding MRO and super() ===")

class A:
    def method(self):
        return "A.method"
    
    def common(self):
        return "A.common"

class B(A):
    def method(self):
        return "B.method"
    
    def common(self):
        # Call parent's method using super()
        parent_result = super().common()
        return f"B.common (calling parent: {parent_result})"

class C(A):
    def method(self):
        return "C.method"
    
    def common(self):
        parent_result = super().common()
        return f"C.common (calling parent: {parent_result})"

class D(B, C):
    def method(self):
        # Call B's method
        b_result = B.method(self)
        # Call C's method
        c_result = C.method(self)
        return f"D.method (B says: {b_result}, C says: {c_result})"
    
    def common(self):
        # super() will follow MRO: D -> B -> C -> A
        return super().common()

# Create instance
d = D()

print("Method calls:")
print(f"d.method(): {d.method()}")
print(f"d.common(): {d.common()}")

print("\nMRO for class D:")
for i, cls in enumerate(D.__mro__):
    print(f"  {i}: {cls.__name__}")

print("\nUnderstanding super() calls:")
print("When d.common() is called:")
print("  1. D.common() calls super().common()")
print("  2. super() in D points to B (next in MRO)")
print("  3. B.common() calls super().common()")
print("  4. super() in B points to C (next in MRO)")
print("  5. C.common() calls super().common()")
print("  6. super() in C points to A (next in MRO)")
print("  7. A.common() returns 'A.common'")
print("  8. Results bubble back up the chain")

# Practical example with __init__
print("\n=== Practical super() in __init__ ===")

class Person:
    def __init__(self, name, age):
        print(f"Person.__init__ called with name={name}, age={age}")
        self.name = name
        self.age = age

class Employee(Person):
    def __init__(self, name, age, employee_id):
        print(f"Employee.__init__ called with employee_id={employee_id}")
        super().__init__(name, age)
        self.employee_id = employee_id

class Manager(Employee):
    def __init__(self, name, age, employee_id, department):
        print(f"Manager.__init__ called with department={department}")
        super().__init__(name, age, employee_id)
        self.department = department

print("\nCreating Manager instance:")
mgr = Manager("Alice", 35, "MGR001", "Engineering")

print(f"\nManager attributes:")
print(f"  Name: {mgr.name}")
print(f"  Age: {mgr.age}")
print(f"  Employee ID: {mgr.employee_id}")
print(f"  Department: {mgr.department}")

# C3 Linearization Algorithm
print("\n=== C3 Linearization (Python's MRO algorithm) ===")
print("Python uses C3 linearization to compute MRO.")
print("Rules:")
print("  1. Children precede their parents")
print("  2. Left parents precede right parents")
print("  3. Consistency across all parent classes")

print("\nExample: class D(B, C):")
print("MRO computation:")
print("  L[D] = D + merge(L[B], L[C], B, C)")
print("  L[B] = B + merge(L[A], A) = B + merge(A, A) = B, A")
print("  L[C] = C + merge(L[A], A) = C + merge(A, A) = C, A")
print("  L[D] = D + merge(B,A, C,A, B, C)")
print("       = D + B + merge(A, C,A, C)")
print("       = D + B + C + merge(A, A)")
print("       = D + B + C + A")
print("  Final MRO: D, B, C, A")

Real-World Applications

Inheritance and polymorphism are used extensively in real-world applications. Here are practical examples:

E-commerce System

Product hierarchy with inheritance:

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    
    def display(self):
        return f"{self.name}: ${self.price}"

class Electronic(Product):
    def __init__(self, name, price, warranty):
        super().__init__(name, price)
        self.warranty = warranty
    
    def display(self):
        return f"{super().display()}, Warranty: {self.warranty} months"

class Book(Product):
    def __init__(self, name, price, author):
        super().__init__(name, price)
        self.author = author
    
    def display(self):
        return f"{super().display()}, Author: {self.author}"
Game Development

Character inheritance hierarchy:

class GameObject:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def draw(self):
        pass
    
    def update(self):
        pass

class Character(GameObject):
    def __init__(self, x, y, health):
        super().__init__(x, y)
        self.health = health
    
    def take_damage(self, damage):
        self.health -= damage

class Player(Character):
    def draw(self):
        return "Drawing player"
    
    def update(self):
        return "Updating player based on input"

class Enemy(Character):
    def draw(self):
        return "Drawing enemy"
    
    def update(self):
        return "Updating enemy AI"
GUI Framework

Widget inheritance structure:

class Widget:
    def __init__(self, x, y, width, height):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
    
    def draw(self):
        return f"Drawing widget at ({self.x}, {self.y})"

class Button(Widget):
    def __init__(self, x, y, width, height, text):
        super().__init__(x, y, width, height)
        self.text = text
    
    def draw(self):
        return f"Drawing button '{self.text}'"
    
    def click(self):
        return f"Button '{self.text}' clicked"

class TextBox(Widget):
    def __init__(self, x, y, width, height, placeholder):
        super().__init__(x, y, width, height)
        self.placeholder = placeholder
        self.text = ""
    
    def draw(self):
        return f"Drawing text box"
Database ORM

Model inheritance patterns:

class Model:
    def __init__(self):
        self.created_at = datetime.now()
    
    def save(self):
        return f"Saving {self.__class__.__name__}"
    
    def delete(self):
        return f"Deleting {self.__class__.__name__}"

class User(Model):
    def __init__(self, username, email):
        super().__init__()
        self.username = username
        self.email = email
    
    def save(self):
        # Custom save logic for User
        result = super().save()
        return f"{result} with username {self.username}"

class Admin(User):
    def __init__(self, username, email, permissions):
        super().__init__(username, email)
        self.permissions = permissions

Best Practices and Design Patterns

Inheritance Best Practices
  • Use inheritance for "is-a" relationships
  • Prefer composition over inheritance for "has-a" relationships
  • Keep inheritance hierarchies shallow (2-3 levels max)
  • Use abstract classes for common interfaces
  • Document the purpose of each class in the hierarchy
Common Pitfalls
  • Avoid deep inheritance chains (hard to maintain)
  • Don't use inheritance just for code reuse
  • Watch for the fragile base class problem
  • Be careful with multiple inheritance complexity
  • Don't override methods without calling super() when needed
Design Patterns Using Inheritance:
  • Template Method Pattern: Define algorithm skeleton in base class, let subclasses override specific steps
  • Factory Method Pattern: Base class defines interface, subclasses decide which class to instantiate
  • Strategy Pattern: Define family of algorithms, make them interchangeable
  • Decorator Pattern: Add responsibilities to objects dynamically
  • Composite Pattern: Treat individual objects and compositions uniformly

Practice Exercises

Test your inheritance knowledge with these exercises. Try to solve them before looking at solutions.

Inheritance Exercises
# Python Inheritance Practice Exercises

print("=== Exercise 1: Employee Hierarchy ===")
# Create a class hierarchy for different types of employees

class Employee:
    def __init__(self, name, employee_id):
        self.name = name
        self.employee_id = employee_id
    
    def calculate_salary(self):
        raise NotImplementedError("Subclass must implement this method")
    
    def display_info(self):
        return f"ID: {self.employee_id}, Name: {self.name}"

class FullTimeEmployee(Employee):
    def __init__(self, name, employee_id, monthly_salary):
        super().__init__(name, employee_id)
        self.monthly_salary = monthly_salary
    
    def calculate_salary(self):
        return self.monthly_salary

class PartTimeEmployee(Employee):
    def __init__(self, name, employee_id, hourly_rate, hours_worked):
        super().__init__(name, employee_id)
        self.hourly_rate = hourly_rate
        self.hours_worked = hours_worked
    
    def calculate_salary(self):
        return self.hourly_rate * self.hours_worked

class ContractEmployee(Employee):
    def __init__(self, name, employee_id, contract_amount):
        super().__init__(name, employee_id)
        self.contract_amount = contract_amount
    
    def calculate_salary(self):
        return self.contract_amount

# Test the hierarchy
employees = [
    FullTimeEmployee("Alice", "FT001", 5000),
    PartTimeEmployee("Bob", "PT001", 20, 80),
    ContractEmployee("Charlie", "CT001", 10000)
]

print("Employee Salaries:")
for emp in employees:
    print(f"{emp.name}: ${emp.calculate_salary():,.2f}")

print("\n=== Exercise 2: Shape Hierarchy ===")
# Create shape hierarchy with area calculation

from math import pi

class Shape:
    def area(self):
        raise NotImplementedError
    
    def perimeter(self):
        raise NotImplementedError

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height
    
    def perimeter(self):
        return 2 * (self.width + self.height)

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return pi * self.radius ** 2
    
    def perimeter(self):
        return 2 * pi * self.radius

class Triangle(Shape):
    def __init__(self, base, height, side1, side2, side3):
        self.base = base
        self.height = height
        self.side1 = side1
        self.side2 = side2
        self.side3 = side3
    
    def area(self):
        return 0.5 * self.base * self.height
    
    def perimeter(self):
        return self.side1 + self.side2 + self.side3

# Test shapes
shapes = [
    Rectangle(4, 5),
    Circle(3),
    Triangle(6, 4, 3, 4, 5)
]

print("Shape Areas and Perimeters:")
for shape in shapes:
    print(f"{shape.__class__.__name__}: Area={shape.area():.2f}, Perimeter={shape.perimeter():.2f}")

print("\n=== Exercise 3: Bank Account Hierarchy ===")
# Create bank account hierarchy

class BankAccount:
    def __init__(self, account_number, balance=0):
        self.account_number = account_number
        self.balance = balance
    
    def deposit(self, amount):
        self.balance += amount
        return f"Deposited ${amount}. New balance: ${self.balance}"
    
    def withdraw(self, amount):
        if amount > self.balance:
            return "Insufficient funds"
        self.balance -= amount
        return f"Withdrew ${amount}. New balance: ${self.balance}"
    
    def display_balance(self):
        return f"Account {self.account_number}: Balance ${self.balance}"

class SavingsAccount(BankAccount):
    def __init__(self, account_number, balance=0, interest_rate=0.01):
        super().__init__(account_number, balance)
        self.interest_rate = interest_rate
    
    def add_interest(self):
        interest = self.balance * self.interest_rate
        self.balance += interest
        return f"Added ${interest:.2f} interest. New balance: ${self.balance:.2f}"

class CheckingAccount(BankAccount):
    def __init__(self, account_number, balance=0, overdraft_limit=100):
        super().__init__(account_number, balance)
        self.overdraft_limit = overdraft_limit
    
    def withdraw(self, amount):
        if amount > self.balance + self.overdraft_limit:
            return "Overdraft limit exceeded"
        self.balance -= amount
        return f"Withdrew ${amount}. New balance: ${self.balance}"

# Test bank accounts
print("Bank Account Operations:")
savings = SavingsAccount("SAV001", 1000, 0.02)
checking = CheckingAccount("CHK001", 500, 200)

print(savings.display_balance())
print(savings.deposit(500))
print(savings.add_interest())

print(f"\n{checking.display_balance()}")
print(checking.withdraw(600))
print(checking.withdraw(200))

print("\n=== Exercise 4: Vehicle Rental System ===")
# Multiple inheritance example

class Vehicle:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
    
    def display_info(self):
        return f"{self.year} {self.make} {self.model}"

class Rentable:
    def __init__(self, daily_rate):
        self.daily_rate = daily_rate
        self.is_rented = False
    
    def rent(self, days):
        if self.is_rented:
            return "Already rented"
        self.is_rented = True
        return f"Rented for {days} days. Total: ${self.daily_rate * days}"
    
    def return_vehicle(self):
        self.is_rented = False
        return "Vehicle returned"

class Car(Vehicle, Rentable):
    def __init__(self, make, model, year, daily_rate, doors):
        Vehicle.__init__(self, make, model, year)
        Rentable.__init__(self, daily_rate)
        self.doors = doors
    
    def display_info(self):
        vehicle_info = Vehicle.display_info(self)
        return f"{vehicle_info} with {self.doors} doors"

# Test rental system
print("Vehicle Rental System:")
car = Car("Toyota", "Camry", 2023, 50, 4)
print(car.display_info())
print(car.rent(3))
print(car.return_vehicle())

print("\n=== Exercise 5: Abstract Animal Zoo ===")
# Abstract classes and polymorphism

from abc import ABC, abstractmethod

class Animal(ABC):
    def __init__(self, name, species):
        self.name = name
        self.species = species
    
    @abstractmethod
    def make_sound(self):
        pass
    
    @abstractmethod
    def move(self):
        pass
    
    def display_info(self):
        return f"{self.name} the {self.species}"

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} is giving birth to live young"

class Bird(Animal):
    def __init__(self, name, species, wingspan):
        super().__init__(name, species)
        self.wingspan = wingspan
    
    def lay_eggs(self):
        return f"{self.name} is laying eggs"

class Lion(Mammal):
    def make_sound(self):
        return "Roar!"
    
    def move(self):
        return "Walking on four legs"

class Eagle(Bird):
    def make_sound(self):
        return "Screech!"
    
    def move(self):
        return "Flying through the air"

# Test zoo
print("Zoo Animals:")
animals = [Lion("Simba", "Lion", "Golden"), Eagle("Freedom", "Eagle", 2.5)]

for animal in animals:
    print(f"\n{animal.display_info()}")
    print(f"Sound: {animal.make_sound()}")
    print(f"Movement: {animal.move()}")
    if isinstance(animal, Mammal):
        print(animal.give_birth())
    if isinstance(animal, Bird):
        print(animal.lay_eggs())

Key Takeaways

  • Inheritance allows child classes to reuse and extend parent class functionality
  • Python supports multiple inheritance using comma-separated parent classes
  • Use the super() function to call parent class methods
  • Method Resolution Order (MRO) determines the search order for methods in inheritance hierarchy
  • Method overriding allows child classes to provide specific implementations
  • Polymorphism enables objects of different classes to be treated as objects of a common superclass
  • Abstract classes (using abc module) define interfaces that must be implemented by subclasses
  • Duck typing in Python focuses on object behavior rather than type
  • Operator overloading allows defining behavior for operators like +, -, *
  • Use inheritance for "is-a" relationships and composition for "has-a" relationships
  • Keep inheritance hierarchies shallow to avoid complexity
  • The C3 linearization algorithm computes MRO in Python
  • Mixins are small classes that provide specific functionality to be inherited
  • Always document the purpose and relationships in your class hierarchy