Skip to content

Inheritance Basics

Inheritance allows classes to reuse and extend behavior. super() enables cooperative method calls.


Basic Inheritance

class Animal:
    def speak(self):
        print("sound")

class Dog(Animal):
    def speak(self):
        print("bark")

Calling Parent Methods

class LoggedDog(Dog):
    def speak(self):
        super().speak()
        print("logged")

super() respects the MRO.


Why Use super()

1. Multiple Inheritance

Enables cooperative inheritance patterns.

2. Avoids Hardcoding

No need to hardcode parent class names.

3. Extensible Design

Makes refactoring easier and designs more flexible.


Best Practices

1. Use super() Always

In cooperative hierarchies, always use super().

2. Keep Shallow

Prefer shallow inheritance hierarchies.

3. Prefer Composition

Use composition when inheritance isn't needed.


Key Takeaways

  • Inheritance extends behavior.
  • super() follows the MRO.
  • Proper use enables flexible designs.

Runnable Example: inheritance_examples.py

"""
Example 01: Basic Inheritance

This example demonstrates the fundamental concept of inheritance,
where a child class inherits attributes and methods from a parent class.
"""

# Parent Class (also called Base Class or Superclass)

# =============================================================================
# Definitions
# =============================================================================

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

    def info(self):
        return f"{self.name} is a {self.species}."


# Child Class (also called Derived Class or Subclass)
class Dog(Animal):
    def __init__(self, name, breed):
        # Call parent constructor
        super().__init__(name, "Dog")
        self.breed = breed

    # Dog-specific method
    def bark(self):
        return f"{self.name} says Woof!"


class Cat(Animal):
    def __init__(self, name, color):
        super().__init__(name, "Cat")
        self.color = color

    # Cat-specific method
    def meow(self):
        return f"{self.name} says Meow!"


# Testing the classes

# =============================================================================
# Main
# =============================================================================

if __name__ == "__main__":
    print("=" * 50)
    print("BASIC INHERITANCE DEMO")
    print("=" * 50)

    # Create a Dog object
    dog = Dog("Buddy", "Golden Retriever")
    print(f"\nDog Name: {dog.name}")
    print(f"Breed: {dog.breed}")
    print(dog.info())  # Inherited method
    print(dog.eat())   # Inherited method
    print(dog.bark())  # Dog-specific method

    # Create a Cat object
    cat = Cat("Whiskers", "Orange")
    print(f"\nCat Name: {cat.name}")
    print(f"Color: {cat.color}")
    print(cat.info())  # Inherited method
    print(cat.sleep()) # Inherited method
    print(cat.meow())  # Cat-specific method

    # Check inheritance
    print("\n" + "=" * 50)
    print("INHERITANCE VERIFICATION")
    print("=" * 50)
    print(f"Is dog an Animal? {isinstance(dog, Animal)}")
    print(f"Is dog a Dog? {isinstance(dog, Dog)}")
    print(f"Is cat an Animal? {isinstance(cat, Animal)}")
    print(f"Is dog a Cat? {isinstance(dog, Cat)}")

"""
KEY TAKEAWAYS:
1. Child classes inherit all attributes and methods from parent class
2. Use super() to call parent class constructor
3. Child classes can have their own unique methods
4. isinstance() checks if an object is an instance of a class
5. A child class object is also an instance of its parent class
"""

Runnable Example: method_overriding_examples.py

"""
Example 02: Method Overriding

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

# =============================================================================
# Definitions
# =============================================================================

class Shape:
    def __init__(self, name):
        self.name = name

    def area(self):
        return 0  # Default implementation

    def perimeter(self):
        return 0  # Default implementation

    def describe(self):
        return f"This is a {self.name}"


class Rectangle(Shape):
    def __init__(self, width, height):
        super().__init__("Rectangle")
        self.width = width
        self.height = height

    # Override area method
    def area(self):
        return self.width * self.height

    # Override perimeter method
    def perimeter(self):
        return 2 * (self.width + self.height)

    # Override describe method
    def describe(self):
        # Call parent's describe and add more info
        parent_desc = super().describe()
        return f"{parent_desc} with width {self.width} and height {self.height}"


class Circle(Shape):
    def __init__(self, radius):
        super().__init__("Circle")
        self.radius = radius

    # Override area method
    def area(self):
        return 3.14159 * self.radius ** 2

    # Override perimeter method (circumference)
    def perimeter(self):
        return 2 * 3.14159 * self.radius

    # Override describe method
    def describe(self):
        return f"{super().describe()} with radius {self.radius}"


class Triangle(Shape):
    def __init__(self, base, height, side1, side2, side3):
        super().__init__("Triangle")
        self.base = base
        self.height = height
        self.side1 = side1
        self.side2 = side2
        self.side3 = side3

    # Override area method
    def area(self):
        return 0.5 * self.base * self.height

    # Override perimeter method
    def perimeter(self):
        return self.side1 + self.side2 + self.side3


# Testing method overriding

# =============================================================================
# Main
# =============================================================================

if __name__ == "__main__":
    print("=" * 60)
    print("METHOD OVERRIDING DEMO")
    print("=" * 60)

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

    # Process each shape
    for shape in shapes:
        print(f"\n{shape.describe()}")
        print(f"  Area: {shape.area():.2f}")
        print(f"  Perimeter: {shape.perimeter():.2f}")

    # Demonstrate that the same method name gives different results
    print("\n" + "=" * 60)
    print("SAME METHOD, DIFFERENT BEHAVIOR")
    print("=" * 60)

    rect = Rectangle(4, 5)
    circ = Circle(3)

    print(f"\nrect.area() = {rect.area()}")
    print(f"circ.area() = {circ.area():.2f}")
    print("\nSame method name 'area()', but different calculations!")

"""
KEY TAKEAWAYS:
1. Child classes can override parent methods to provide specific behavior
2. Use super() to call the parent's version of the method if needed
3. Method overriding is the foundation of polymorphism
4. The overridden method must have the same name as the parent's method
5. You can completely replace or extend the parent's functionality
"""

Runnable Example: super_function_examples.py

"""
Example 03: Using super() Function

The super() function is used to call methods from the parent class.
This is especially useful when you want to extend (not replace) parent functionality.
"""

# =============================================================================
# Definitions
# =============================================================================

class Employee:
    def __init__(self, name, employee_id, salary):
        self.name = name
        self.employee_id = employee_id
        self.salary = salary
        print(f"Employee.__init__ called for {name}")

    def get_details(self):
        return f"ID: {self.employee_id}, Name: {self.name}, Salary: ${self.salary}"

    def calculate_bonus(self):
        return self.salary * 0.05  # 5% base bonus


class Manager(Employee):
    def __init__(self, name, employee_id, salary, department):
        # Call parent constructor using super()
        super().__init__(name, employee_id, salary)
        self.department = department
        self.team_size = 0
        print(f"Manager.__init__ called for {name}")

    def get_details(self):
        # Extend parent's method
        parent_details = super().get_details()
        return f"{parent_details}, Department: {self.department}, Team Size: {self.team_size}"

    def calculate_bonus(self):
        # Extend parent's calculation
        base_bonus = super().calculate_bonus()
        management_bonus = self.salary * 0.10  # Additional 10% for managers
        return base_bonus + management_bonus


class Developer(Employee):
    def __init__(self, name, employee_id, salary, programming_languages):
        super().__init__(name, employee_id, salary)
        self.programming_languages = programming_languages
        self.projects_completed = 0
        print(f"Developer.__init__ called for {name}")

    def get_details(self):
        parent_details = super().get_details()
        langs = ", ".join(self.programming_languages)
        return f"{parent_details}, Languages: {langs}, Projects: {self.projects_completed}"

    def calculate_bonus(self):
        base_bonus = super().calculate_bonus()
        # Bonus per project completed
        project_bonus = self.projects_completed * 500
        return base_bonus + project_bonus


class TechLead(Manager, Developer):
    """
    A TechLead is both a Manager and a Developer
    This demonstrates multiple inheritance and super() with MRO
    """
    def __init__(self, name, employee_id, salary, department, programming_languages):
        # super() handles the complex inheritance chain
        Manager.__init__(self, name, employee_id, salary, department)
        self.programming_languages = programming_languages
        self.projects_completed = 0
        print(f"TechLead.__init__ called for {name}")

    def get_details(self):
        # Get base employee details
        base_details = Employee.get_details(self)
        langs = ", ".join(self.programming_languages)
        return f"{base_details}, Department: {self.department}, Languages: {langs}"

    def calculate_bonus(self):
        # Combines bonuses from both Manager and Developer roles
        base_bonus = Employee.calculate_bonus(self)
        management_bonus = self.salary * 0.10
        project_bonus = self.projects_completed * 500
        return base_bonus + management_bonus + project_bonus


# Testing super() usage

# =============================================================================
# Main
# =============================================================================

if __name__ == "__main__":
    print("=" * 70)
    print("SUPER() FUNCTION DEMO")
    print("=" * 70)

    print("\n1. Creating a Manager:")
    print("-" * 70)
    manager = Manager("Alice Johnson", "M001", 80000, "Engineering")
    manager.team_size = 5
    print(manager.get_details())
    print(f"Bonus: ${manager.calculate_bonus():.2f}")

    print("\n2. Creating a Developer:")
    print("-" * 70)
    dev = Developer("Bob Smith", "D001", 75000, ["Python", "JavaScript", "Go"])
    dev.projects_completed = 8
    print(dev.get_details())
    print(f"Bonus: ${dev.calculate_bonus():.2f}")

    print("\n3. Creating a TechLead (Multiple Inheritance):")
    print("-" * 70)
    tech_lead = TechLead("Carol Williams", "TL001", 95000, "Backend", ["Python", "Java"])
    tech_lead.team_size = 3
    tech_lead.projects_completed = 5
    print(tech_lead.get_details())
    print(f"Bonus: ${tech_lead.calculate_bonus():.2f}")

    print("\n" + "=" * 70)
    print("METHOD RESOLUTION ORDER (MRO)")
    print("=" * 70)
    print(f"TechLead MRO: {[cls.__name__ for cls in TechLead.__mro__]}")

"""
KEY TAKEAWAYS:
1. super() calls the parent class methods
2. Use super() to extend (not replace) parent functionality
3. super() is essential in __init__ to properly initialize parent classes
4. With multiple inheritance, super() follows the Method Resolution Order (MRO)
5. super() makes your code more maintainable and flexible
6. You can call parent methods explicitly, but super() is usually better

COMMON PATTERNS:
- super().__init__(...) - Initialize parent in child __init__
- super().method() - Call parent's method before/after child's logic
- parent_result = super().method() - Get parent's result and extend it
"""