Previous | Tutorial index | Next

📘 TUTORIAL 4: USING SUBCLASSES AND SUPERCLASSES PROPERLY

Learning Objective

Understand and implement inheritance relationships between classes.

4.1 What is Inheritance? The "Is‑A" Relationship

Inheritance is a mechanism that allows a new class (called a subclass or child class) to acquire the properties and behaviours of an existing class (called a superclass or parent class). The subclass is a specialized version of the superclass—it is‑a more specific type of the parent.

The "Is‑A" Relationship

This relationship reflects how we naturally categorise things in the real world. Inheritance models this hierarchical classification in code.

Why Use Inheritance?

  1. Code Reusability: Common attributes and methods are written once in the superclass and reused by all subclasses.
  2. Extensibility: Subclasses can add new features without modifying the superclass.
  3. Polymorphism: Subclasses can be treated as instances of the superclass, enabling flexible and generic code.
  4. Logical Organisation: Inheritance creates a clear, hierarchical structure that mirrors the problem domain.

4.2 Syntax: Declaring a Subclass

To declare a subclass, you place the superclass name in parentheses after the subclass name:

class Superclass: # ... class body ... class Subclass(Superclass): # ... class body ...

Example: Basic Inheritance

class Animal: def __init__(self, name): self.name = name def eat(self): return f"{self.name} is eating." def sleep(self): return f"{self.name} is sleeping." class Dog(Animal): # Dog inherits __init__, eat(), and sleep() automatically pass # Creating instances fido = Dog("Fido") print(fido.eat()) # Fido is eating. (inherited) print(fido.sleep()) # Fido is sleeping. (inherited) print(fido.name) # Fido (inherited attribute)

Even though the Dog class is empty (pass), it automatically has all the attributes and methods defined in Animal. This is the power of inheritance.

Multiple Levels of Inheritance

Inheritance can be chained to create deeper hierarchies.

class Animal: def __init__(self, name): self.name = name class Mammal(Animal): def __init__(self, name, fur_color): super().__init__(name) # Call Animal's __init__ self.fur_color = fur_color def nurse_young(self): return f"{self.name} is nursing." class Dog(Mammal): def __init__(self, name, fur_color, breed): super().__init__(name, fur_color) # Call Mammal's __init__ self.breed = breed def bark(self): return f"{self.name} says woof!" # Now Dog inherits from Mammal, which inherits from Animal. # Dog has: name, fur_color, breed, eat(), sleep(), nurse_young(), bark().

4.3 Inheriting Attributes and Methods

When you inherit from a superclass, the subclass automatically receives:

What Is Not Inherited?

Example: Inherited Methods in Action

class Vehicle: def __init__(self, make, model, year): self.make = make self.model = model self.year = year def start(self): return "Engine started." def stop(self): return "Engine stopped." class Car(Vehicle): def __init__(self, make, model, year, doors): super().__init__(make, model, year) self.doors = doors def honk(self): return "Beep beep!" my_car = Car("Toyota", "Camry", 2022, 4) print(my_car.start()) # Engine started. (inherited) print(my_car.honk()) # Beep beep! (new method) print(my_car.make) # Toyota (inherited attribute)

4.4 Method Overriding: Redefining Behaviour

Sometimes, a subclass needs to provide a different implementation of a method that already exists in the superclass. This is called method overriding.

Syntax for Overriding

Simply define a method with the same name in the subclass. The subclass's version will be called instead of the superclass's version.

class Animal: def speak(self): return "Some generic animal sound." class Dog(Animal): def speak(self): # Override the superclass method return "Woof!" class Cat(Animal): def speak(self): # Override again return "Meow!" animals = [Animal(), Dog(), Cat()] for a in animals: print(a.speak())

Output:

Some generic animal sound. Woof! Meow!

Why Override?

Calling the Overridden Method

You can still access the superclass's version using super() (see Section 4.5).

4.5 Method Specialization: Extending Parent Behaviour

Often, you don't want to completely replace the superclass method—you want to add to it. This is called method specialization or extension.

Using super() to Call the Parent's Method

The super() function returns a proxy object that allows you to call methods from the superclass. This is essential for extending behaviour.

class Animal: def speak(self): return "Animal sound" class Dog(Animal): def speak(self): parent_sound = super().speak() # Call parent method return f"Dog says: {parent_sound} and then barks!" d = Dog() print(d.speak()) # Dog says: Animal sound and then barks!

Common Use Case: Extending __init__

When you add new attributes in a subclass, you must call the superclass's __init__ to ensure that inherited attributes are properly initialized.

class Person: def __init__(self, name, age): self.name = name self.age = age class Student(Person): def __init__(self, name, age, student_id): super().__init__(name, age) # Initialize name and age self.student_id = student_id # New attribute s = Student("Alice", 20, "S123") print(s.name) # Alice print(s.student_id) # S123

If you forget super().__init__(), the name and age attributes would not be created, leading to an AttributeError when you try to access them.

4.6 The super() Function in Depth

super() is a built‑in function that returns a temporary object of the superclass, allowing you to call its methods. It is used extensively in inheritance to avoid hardcoding the superclass name, making code more maintainable and supporting multiple inheritance.

Two Common Forms

  1. super().__init__() – inside a method, calls the superclass's method (most common).
  2. super(Subclass, self).method() – the older, explicit form (rarely needed now).

Why Use super() Instead of the Superclass Name?

Example: Proper Use of super() in a Chain

class A: def __init__(self): print("A init") super().__init__() # Even though A has no parent, this is safe class B(A): def __init__(self): print("B init") super().__init__() class C(B): def __init__(self): print("C init") super().__init__() c = C() # Output: # C init # B init # A init

Notice that super() in A does nothing (calls object.__init__) but doesn't raise an error. This is why it's safe to call super().__init__() even in the topmost class.

4.7 The Method Resolution Order (MRO)

When a method is called on an instance, Python searches for it in a specific order: the instance's class, then its parent class, then the parent's parent, and so on, up to object (the ultimate base class). This search order is called the Method Resolution Order (MRO).

Viewing the MRO

You can inspect the MRO of any class using the __mro__ attribute or the mro() method.

class Animal: pass class Mammal(Animal): pass class Dog(Mammal): pass print(Dog.__mro__) # (<class '__main__.Dog'>, <class '__main__.Mammal'>, <class '__main__.Animal'>, <class 'object'>)

Why MRO Matters

4.8 Multiple Inheritance (Brief Overview)

Python supports multiple inheritance, where a subclass can inherit from more than one superclass.

class Flyer: def fly(self): return "Flying..." class Swimmer: def swim(self): return "Swimming..." class Duck(Flyer, Swimmer): pass d = Duck() print(d.fly()) # Flying... print(d.swim()) # Swimming...

Potential Issues: The Diamond Problem

When two superclasses have a method with the same name, the MRO determines which one is used. Python uses the C3 linearization algorithm to create a consistent 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): pass d = D() d.method() # Output: B (because B appears before C in MRO) print(D.__mro__) # (D, B, C, A, object)

Best Practice: Use multiple inheritance sparingly. Prefer composition over inheritance when possible. If you do use it, be aware of the MRO and design your classes cooperatively (always call super()).

4.9 Common Pitfalls with Inheritance

Pitfall Explanation How to Avoid
Forgetting to call super().__init__() Inherited attributes are not initialized, leading to AttributeError. Always call super().__init__() in __init__ of every subclass.
Calling super() in the wrong place Placing super() after assigning subclass attributes can skip parent initialization. Call super().__init__() at the beginning of your __init__ (or at least before using inherited attributes).
Overriding a method without calling super() when needed You lose the parent's behaviour when you intended to extend it. If you want to extend, use super().method() inside your override.
Using multiple inheritance without understanding MRO Method calls can become unpredictable. Avoid complex multiple inheritance; use mixins for simple, focused behaviour.
Changing the superclass name Hardcoding ParentClass.method(self) breaks if you rename the parent. Always use super() instead of hardcoding the parent class name.
Creating deep inheritance hierarchies Overly deep hierarchies become fragile and hard to maintain. Prefer shallow hierarchies; use composition (has‑a) when appropriate.

4.10 Full Walkthrough Example: A Shape Hierarchy

Let's build a complete example with inheritance, method overriding, and super().

import math class Shape: """Base class for all shapes.""" def __init__(self, color="black"): self.color = color def area(self): raise NotImplementedError("Subclasses must implement area()") def perimeter(self): raise NotImplementedError("Subclasses must implement perimeter()") def __str__(self): return f"{self.__class__.__name__} (color={self.color})" class Rectangle(Shape): def __init__(self, width, height, color="black"): super().__init__(color) # Initialize color self.width = width self.height = height def area(self): return self.width * self.height def perimeter(self): return 2 * (self.width + self.height) def __str__(self): return f"Rectangle(width={self.width}, height={self.height}, color={self.color})" class Square(Rectangle): def __init__(self, side, color="black"): super().__init__(side, side, color) # Reuse Rectangle's __init__ def __str__(self): return f"Square(side={self.side}, color={self.color})" @property def side(self): return self.width # Since width and height are equal class Circle(Shape): def __init__(self, radius, color="black"): super().__init__(color) self.radius = radius def area(self): return math.pi * self.radius ** 2 def perimeter(self): return 2 * math.pi * self.radius # --- Using the hierarchy --- shapes = [ Rectangle(5, 3, "red"), Square(4, "blue"), Circle(2.5, "green") ] for shape in shapes: print(shape) print(f" Area: {shape.area():.2f}") print(f" Perimeter: {shape.perimeter():.2f}\n")

Output:

Rectangle(width=5, height=3, color=red) Area: 15.00 Perimeter: 16.00 Square(side=4, color=blue) Area: 16.00 Perimeter: 16.00 Circle(radius=2.5, color=green) Area: 19.63 Perimeter: 15.71

Key Observations:


📝 Quiz 4: Inheritance and super()

Answer the following questions to check your understanding.

1. What is the correct syntax for declaring a class Dog that inherits from Animal?

Answer(B) `class Dog(Animal):`

2. When a subclass overrides a method, the subclass's version:

Answer(B) Completely replaces the superclass version unless `super()` is used.

3. What does super().__init__() do in a subclass?

Answer(B) It calls the initializer of the superclass to initialize inherited attributes.

4. Consider the following code:

class A: def __init__(self): self.x = 1 class B(A): def __init__(self): self.y = 2 b = B() print(b.x)

What happens?

Answer(C) Raises an AttributeError because `x` was never initialized (missing `super().__init__()`).

5. (True/False) Private attributes (with double underscore __) are inherited by subclasses.

Answer(B) False – they are name‑mangled and not directly accessible.

6. Which of the following is a valid use of super()?

Answer(C) Both A and B

7. The Method Resolution Order (MRO) determines:

Answer(B) The order in which methods are searched for in the inheritance hierarchy.

8. What is the output of the following code?

class Parent: def greet(self): return "Hello from Parent" class Child(Parent): def greet(self): return "Hello from Child" c = Child() print(c.greet())
Answer(B) "Hello from Child"

🧪 Exercise 4: Building an Inheritance Hierarchy

Part A: Create a Class Hierarchy for Employees

Design three classes:

Part B: Instantiate and Test

Create one Employee, one Manager, and one Developer. Call display() and calculate_salary() on each. Ensure you use super() correctly to avoid duplicating code.

Part C: Reflection Question
In your code comments, answer: "Why is it beneficial to have calculate_salary() defined in Employee and overridden in subclasses rather than having completely separate methods?" (Hint: Polymorphism.)

Sample Answer Defining `calculate_salary()` in the superclass and overriding it in subclasses allows us to treat all employees polymorphically. For example, we can store a list of `Employee` objects (which may include `Manager` and `Developer` instances) and call `calculate_salary()` on each; the correct version (based on the actual object type) will be executed automatically. This makes the code more flexible and extensible, as new employee types can be added without changing the existing iteration logic.

🏠 Homework 4: Building a Zoo Management System

Task: You are developing a software system for a zoo. Create a complete Python script that models animals using inheritance. Follow the specifications below.

Class Hierarchy

Base Class: Animal

Subclass: Mammal (inherits from Animal)

Subclass: Bird (inherits from Animal)

Subclass: Reptile (inherits from Animal)

More Specific Subclasses (choose at least 2 of the following):

Homework Submission Requirements

Part 1: Code
Write the complete class definitions with all methods. Include:

Part 2: Testing Script
In the same file, create a list called zoo_animals containing at least 6 animal objects (from different subclasses). Then:

Part 3: Reflection Questions
Answer these in a comment block at the top of your script:

  1. In this hierarchy, where would you place a Penguin class? Would it inherit from Bird or something else? Why?
  2. What would happen if you forgot to call super().__init__() in the Mammal class? How would that affect Lion?
  3. Why is the Animal class useful even though you will never instantiate it directly? (Hint: abstraction and polymorphism.)
  4. If you added a new method walk() to Animal, how many subclasses would automatically get it? Explain.
Sample Answers (Part 3) 1. A `Penguin` would inherit from `Bird` because it shares bird‑like characteristics (feathers, beak, lays eggs). Even though it cannot fly, it is still a bird; we would override `fly()` to return a message that penguins cannot fly, rather than changing its base class. 2. If we forgot `super().__init__()` in `Mammal`, then `Mammal` would not initialize the `name`, `species`, etc. attributes. When `Lion` calls `super().__init__()`, it only calls `Mammal.__init__` which would not have set up the base attributes, so `Lion` would miss those attributes, causing `AttributeError` when trying to access them later. 3. The `Animal` class defines a common interface (methods like `eat()`, `sleep()`, and `make_sound()`) that all animals must implement. Even if we never create an `Animal` directly, it provides a contract that ensures polymorphism works: we can treat all animals uniformly and call these methods without knowing the specific type. 4. If we added `walk()` to `Animal`, every subclass (Mammal, Bird, Reptile, and all their children) would automatically inherit it. This is because inheritance propagates the method down the hierarchy. Each subclass would then have a `walk()` method unless they choose to override it with a more specific implementation.

Bonus Challenge (Optional):
Add a class attribute total_animals to Animal that increments every time an animal is instantiated. Add a class method get_total_animals() that returns the count. Demonstrate that it works correctly.


📚 Additional Resources for Self‑Study

  1. Real Python: Inheritance and Composition in Python – Excellent coverage with examples.
  2. Python Official Docs: 9.5. Inheritance – The definitive reference.
  3. Programiz: Python Inheritance – Interactive examples.
  4. Indiana University: Competency 11‑1: Inheritance – Academic perspective.
  5. Real Python: super() Considered Super! – Deep dive into super().

✅ Summary Checklist for Tutorial 4

Before moving to Tutorial 5 (Access Modifiers), ensure you can confidently say YES to the following:

Ready for the next tutorial? In Tutorial 5, you will learn about encapsulation and access modifiers—how to use naming conventions to control access to class members and protect data integrity.

Previous | Tutorial index | Next