Programming with Python | Chapter 13: OOP – Inheritance

Chapter Objectives

  • Understand the concept of inheritance in OOP for Python.
  • Differentiate between a base class (superclass/parent) and a derived class (subclass/child).
  • Learn the syntax for creating a derived class that inherits from a base class.
  • Understand how derived classes inherit attributes and methods from their base class.
  • Learn how to override methods from the base class in the derived class.
  • Use the super() function to call methods from the base class within the derived class, especially __init__.
  • Recognize the “is-a” relationship represented by inheritance.
  • Appreciate the benefits of inheritance for code reuse and creating hierarchies.

Introduction

Chapter 12 introduced classes and objects, allowing us to model entities with attributes and methods. Inheritance is a fundamental OOP principle that allows us to create a new class (the derived class or subclass) that inherits properties (attributes and methods) from an existing class (the base class or superclass). This promotes code reuse, as common functionality can be defined in the base class and automatically acquired by derived classes. Derived classes can then add their own unique attributes and methods or modify (override) the inherited behavior to specialize it. Inheritance models an “is-a” relationship (e.g., a Dog is an Animal, a Car is a Vehicle).

Theory & Explanation

What is Inheritance?

Inheritance is a mechanism where one class (the child/subclass/derived class) acquires the properties (attributes and methods) of another class (the parent/superclass/base class). The subclass can then:

  • Use the inherited attributes and methods directly.
  • Add new attributes and methods specific to itself.
  • Override inherited methods to provide specialized behavior.

Analogy: Think of biological inheritance. You inherit traits (like eye color) from your parents, but you also have your own unique characteristics. Similarly, a subclass inherits features from its superclass but can also add its own or modify the inherited ones.

Base Class (Superclass) and Derived Class (Subclass)

  • Base Class (Superclass/Parent): The class whose properties are inherited. It represents a more general concept.
  • Derived Class (Subclass/Child): The class that inherits properties from the base class. It represents a more specialized concept.

classDiagram
    direction LR
    class Animal {
        +string name
        +string species
        +__init__(name, species)
        +speak()
        +move()
    }

    class Dog {
        +string breed
        +__init__(name, breed) // Overrides Animal.__init__
        +speak() // Overrides Animal.speak
        +move() // Overrides Animal.move
    }

    class Cat {
       +__init__(name, species) // Inherits or overrides
       +speak() // Overrides Animal.speak
    }

    Animal <|-- Dog : Inherits
    Animal <|-- Cat : Inherits

    note for Animal "Base Class<br/>(Superclass)"
    note for Dog "Derived Class<br/>(Subclass)<br/>Uses super()"
    note for Cat "Derived Class<br/>(Subclass)"

Syntax for Inheritance

To create a derived class that inherits from a base class, you include the base class name in parentheses after the derived class name in the class definition.

Syntax:

Python
class BaseClassName:
    # Base class definition
    pass # Placeholder for actual code

class DerivedClassName(BaseClassName): # Inherits from BaseClassName
    # Derived class definition
    # Can add new attributes/methods or override existing ones
    pass

Inheriting Attributes and Methods

Animal (Base Class) Dog (Subclass) Cat (Subclass) inherits inherits

By default, a derived class inherits all the attributes and methods of its base class. Objects of the derived class can access these inherited members as if they were defined in the derived class itself (unless they are overridden).

Python
class Animal:
    """Represents a general animal."""
    def __init__(self, name, species):
        self.name = name
        self.species = species
        print(f"Animal initialized: {self.name}")

    def speak(self):
        print("Some generic animal sound")

    def move(self):
        print("Moving...")

# Dog inherits from Animal
class Dog(Animal):
    """Represents a dog, inheriting from Animal."""
    # Dog doesn't define its own __init__ or move yet,
    # so it inherits them from Animal.
    pass # For now, just inherit everything

# Create instances
generic_animal = Animal("Creature", "Unknown")
my_dog = Dog("Buddy", "Canis familiaris") # Calls Animal's __init__

# Access inherited attributes
print(f"Dog's name: {my_dog.name}")       # Inherited from Animal
print(f"Dog's species: {my_dog.species}") # Inherited from Animal

# Call inherited methods
my_dog.move()  # Output: Moving... (Inherited from Animal)
my_dog.speak() # Output: Some generic animal sound (Inherited from Animal)

Overriding Methods

A derived class can provide its own specific implementation of a method that it inherited from its base class. This is called method overriding. To override a method, simply define a method in the derived class with the same name and signature (parameters) as the one in the base class.

Python
class Animal:
    def __init__(self, name, species):
        self.name = name
        self.species = species

    def speak(self): # Base class method
        print("Some generic animal sound")

# Dog inherits from Animal
class Dog(Animal):
    def __init__(self, name, breed): # Dog's own __init__
        # We need to initialize the Animal part too! (See super() next)
        self.name = name # Temporarily set name directly
        self.breed = breed
        print(f"Dog initialized: {self.name}")

    def speak(self): # Overrides Animal's speak method
        print("Woof! Woof!")

# Cat inherits from Animal
class Cat(Animal):
    def speak(self): # Overrides Animal's speak method
        print("Meow!")

# Create instances
d = Dog("Rex", "German Shepherd")
c = Cat("Whiskers", "Felis catus")

d.speak() # Output: Woof! Woof! (Calls Dog's overridden version)
c.speak() # Output: Meow! (Calls Cat's overridden version)

When speak() is called on a Dog object, Python executes the speak method defined in the Dog class. If Dog hadn’t defined speak, Python would have looked up the inheritance chain and executed Animal‘s speak method.

Calling Base Class Methods: super()

Often, when overriding a method (especially __init__), you still want to execute the original implementation from the base class before or after adding the subclass’s specific logic. The super() function provides a way to access methods of the base class (superclass).

  • super().__init__(...): Commonly used within the subclass’s __init__ to call the base class’s __init__, ensuring that the base class attributes are properly initialized.
  • super().method_name(...): Used to call any other method from the base class.
Dog Class def __init__(self, name, breed): super().__init__(name, species=”…”) Animal Class (Base) def __init__(self, name, species): calls base method
Python
class Animal:
    def __init__(self, name, species):
        print(f"Animal __init__ called for {name}")
        self.name = name
        self.species = species

    def move(self):
        print(f"{self.name} is moving.")

class Dog(Animal):
    def __init__(self, name, breed):
        print(f"Dog __init__ called for {name}")
        # Call the __init__ method of the base class (Animal)
        super().__init__(name, species="Canis familiaris")
        # Now add Dog-specific attributes
        self.breed = breed

    def move(self): # Override move
        # Call the base class move method first
        super().move()
        # Add Dog-specific moving behavior
        print(f"{self.name} is running excitedly!")

# Create instance
buddy = Dog("Buddy", "Labrador")
# Output:
# Dog __init__ called for Buddy
# Animal __init__ called for Buddy

print(f"Buddy's species: {buddy.species}") # Initialized by Animal's __init__ via super()
print(f"Buddy's breed: {buddy.breed}")   # Initialized by Dog's __init__

buddy.move()
# Output:
# Buddy is moving. (from super().move())
# Buddy is running excitedly! (from Dog's move())

Using super() is crucial for proper initialization in subclasses and for extending, rather than completely replacing, inherited behavior.

The “is-a” Relationship

Inheritance models an “is-a” relationship. A Dog is an Animal. A Car is a Vehicle. This means an object of the derived class can be treated as an object of the base class in many contexts (this relates to polymorphism, covered later). If the relationship isn’t truly “is-a”, inheritance might not be the best approach (composition might be better – having an object contain another object).

Code Examples

Example 1: Vehicle Hierarchy

Python
# vehicle_hierarchy.py

class Vehicle:
    """Represents a general vehicle."""
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
        self.is_started = False
        print(f"Vehicle created: {self.year} {self.make} {self.model}")

    def start_engine(self):
        if not self.is_started:
            self.is_started = True
            print(f"{self.make} {self.model}'s engine started.")
        else:
            print("Engine is already running.")

    def stop_engine(self):
        if self.is_started:
            self.is_started = False
            print(f"{self.make} {self.model}'s engine stopped.")
        else:
            print("Engine is already stopped.")

    def get_info(self):
        """Returns basic vehicle information."""
        return f"{self.year} {self.make} {self.model}"

# Car inherits from Vehicle
class Car(Vehicle):
    """Represents a car, inheriting from Vehicle."""
    def __init__(self, make, model, year, num_doors):
        # Call parent __init__
        super().__init__(make, model, year)
        # Add car-specific attribute
        self.num_doors = num_doors
        print(f"-> It's a car with {self.num_doors} doors.")

    # Override get_info to add door count
    def get_info(self):
        # Get base info using super()
        base_info = super().get_info()
        return f"{base_info}, Doors: {self.num_doors}"

# Motorcycle inherits from Vehicle
class Motorcycle(Vehicle):
    """Represents a motorcycle, inheriting from Vehicle."""
    def __init__(self, make, model, year, type): # e.g., 'Cruiser', 'Sport'
        super().__init__(make, model, year)
        self.type = type
        print(f"-> It's a {self.type} motorcycle.")

    # Override start_engine for a different sound/message
    def start_engine(self):
        if not self.is_started:
            self.is_started = True
            print(f"{self.make} {self.model}'s engine roars to life!") # Different message
        else:
            print("Engine is already running.")


# --- Using the classes ---
my_car = Car("Toyota", "Corolla", 2021, 4)
my_bike = Motorcycle("Harley-Davidson", "Street Bob", 2023, "Cruiser")

print("\n--- Car Info ---")
print(my_car.get_info())
my_car.start_engine()
my_car.stop_engine()

print("\n--- Motorcycle Info ---")
print(my_bike.get_info())
my_bike.start_engine()
my_bike.start_engine() # Try starting again

classDiagram
    direction LR
    class Vehicle {
        +string make
        +string model
        +int year
        +bool is_started
        +__init__(make, model, year)
        +start_engine()
        +stop_engine()
        +get_info() string
    }

    class Car {
        +int num_doors
        +__init__(make, model, year, num_doors)
        +get_info() string  // Overrides Vehicle.get_info
    }

    class Motorcycle {
        +string type
        +__init__(make, model, year, type)
        +start_engine()  // Overrides Vehicle.start_engine
    }

    Vehicle <|-- Car : Inherits
    Vehicle <|-- Motorcycle : Inherits

    note for Vehicle "Base Class"
    note for Car "Derived Class (Subclass)"
    note for Motorcycle "Derived Class (Subclass)"

Explanation:

  • Vehicle is the base class with common attributes (make, model, year, is_started) and methods (start_engine, stop_engine, get_info).
  • Car inherits from Vehicle. Its __init__ calls super().__init__() to initialize the common attributes and then adds num_doors. It overrides get_info() to include the door count, calling super().get_info() to get the base information.
  • Motorcycle also inherits from Vehicle. Its __init__ calls the parent __init__ and adds a type attribute. It overrides start_engine() to provide a different message.
  • Objects of Car and Motorcycle can use the inherited stop_engine method directly but use their own specialized versions of __init__, get_info (for Car), and start_engine.

Common Mistakes or Pitfalls

  • Forgetting super().__init__(): When a subclass defines its own __init__, forgetting to call super().__init__() means the base class’s initialization logic (setting up its attributes) is skipped, often leading to AttributeError later when inherited methods try to access those missing attributes.
  • Incorrect super() Arguments: Passing the wrong number or type of arguments to super().__init__() or other super().method() calls. The arguments must match what the corresponding base class method expects.
  • Overriding vs. Overloading: Python doesn’t support traditional method overloading (multiple methods with the same name but different parameter types) in the same way as languages like Java or C++. Defining a method with the same name always overrides the base class version. Default arguments or variable arguments (*args, **kwargs) are used instead of overloading.
  • Confusing Inheritance (“is-a”) with Composition (“has-a”): Using inheritance when the relationship isn’t truly “is-a”. For example, a Car has an Engine, but an Engine is not a Car. In this case, the Car class should contain an Engine object as an attribute (composition) rather than inheriting from Engine.

Chapter Summary

Concept Description Syntax / Example
Inheritance Mechanism where a new class (derived/subclass) acquires properties (attributes, methods) from an existing class (base/superclass). Models an “is-a” relationship (e.g., a Dog is an Animal).
Base Class (Superclass) The class whose properties are inherited. Represents a more general concept. class Vehicle: ...
Derived Class (Subclass) The class that inherits properties. Represents a more specialized concept. Can add/override members. class Car(Vehicle): ...
Syntax Specify the base class in parentheses after the derived class name. class DerivedClass(BaseClass):
Method Overriding Providing a specific implementation in the subclass for a method already defined in the base class. The subclass version is used for subclass objects. Defining speak() in Dog overrides speak() from Animal.
super() Function to call methods from the base class within the subclass. Essential for initializing the base part (super().__init__(...)) and extending, not replacing, functionality (super().method(...)). super().__init__(make, model, year)
base_info = super().get_info()
“is-a” Relationship Inheritance signifies that the derived class *is a* type of the base class. If this isn’t true, composition (“has-a”) might be better. A Car is a Vehicle. A Manager is an Employee.
Benefits Code reuse (common logic in base class), extensibility (add specific features in subclass), maintainability (changes in base class affect subclasses), modeling hierarchical relationships. Vehicle hierarchy (Vehicle -> Car, Motorcycle), Animal hierarchy (Animal -> Dog, Cat).
  • Inheritance allows a derived class (subclass) to inherit attributes and methods from a base class (superclass).
  • Use the syntax class DerivedClass(BaseClass): to define inheritance.
  • Subclasses automatically gain access to superclass members.
  • Subclasses can add new attributes and methods.
  • Subclasses can override inherited methods by defining a method with the same name.
  • Use super() to call methods from the base class within the subclass (e.g., super().__init__(...), super().method(...)).
  • Inheritance models an “is-a” relationship and promotes code reuse.

Exercises & Mini Projects

Exercises

  1. Employee and Manager:
    • Create a base class Employee with __init__(self, name, salary) and a method display_employee(self) that prints the name and salary.
    • Create a derived class Manager that inherits from Employee.
    • In the Manager‘s __init__(self, name, salary, department), call the Employee‘s __init__ using super() and add a self.department attribute.
    • Override the display_employee(self) method in Manager to also print the department (it can optionally call the parent’s method using super() first).
    • Create instances of both Employee and Manager and call their display_employee methods.
  2. Shapes:
    • Create a base class Shape with an __init__(self, color) and a method describe(self) that prints “This shape is [color]”.
    • Create a derived class Rectangle inheriting from Shape. Its __init__ should take color, width, and height. Call the parent __init__ and store width/height.
    • Add a method area(self) to Rectangle that returns the area.
    • Override the describe(self) method in Rectangle to print “This rectangle is [color] with width [width] and height [height]”.
    • Create a Rectangle object, call describe(), and print its area.
  3. Calling Parent Method: Using the Employee/Manager classes from Exercise 1, modify the Manager‘s display_employee method so it first calls the Employee‘s display_employee method using super() and then prints the department information on a new line.

Mini Project: Library Items Hierarchy

Goal: Extend the Book class from Chapter 12’s mini-project using inheritance to represent different types of library items.

Steps:

  1. Rename Book to LibraryItem (Base Class):
    • Take the Book class definition from Chapter 12.
    • Rename the class to LibraryItem.
    • Keep the __init__(self, title, identifier) (change isbn to a more generic identifier). Keep is_checked_out.
    • Keep the display_info, check_out, and check_in methods, adjusting them to use identifier if necessary. Make display_info print title and identifier.
  2. Create Book Subclass:
    • Create a new class Book that inherits from LibraryItem.
    • Define its __init__(self, title, identifier, author, num_pages).
    • Inside Book‘s __init__, call the LibraryItem‘s __init__ using super() to initialize title, identifier, and is_checked_out.
    • Add the new instance attributes self.author and self.num_pages.
    • Override the display_info(self) method in Book. It should first call super().display_info() to print the title/identifier, and then print the author and number of pages on new lines.
  3. Create DVD Subclass:
    • Create another class DVD that also inherits from LibraryItem.
    • Define its __init__(self, title, identifier, director, duration_minutes).
    • Call the parent __init__ using super().
    • Add the new instance attributes self.director and self.duration_minutes.
    • Override the display_info(self) method in DVD. Call super().display_info() first, then print the director and duration.
  4. Instantiate and Use:
    • Create at least one Book object and one DVD object.
    • Call display_info() on both objects to see the specialized output.
    • Call check_out() and check_in() on both objects (these methods are inherited directly from LibraryItem and should work without modification).

Additional Sources:

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top