Skip to main content

Inheritance — Code Reuse & Hierarchies

Editorial

Introduction

In the previous tutorials, we built classes, protected their data with encapsulation, and defined contracts with abstraction. But we've been building each class from scratch — duplicating shared logic every time.

Consider a system with Dog, Cat, and Parrot classes. All three have a name, age, and eat() method. Without inheritance, you'd copy-paste those attributes and methods into each class. Change the eat() logic? You'd have to update it in three places.

Inheritance solves this by letting you define shared behavior in a parent class (also called superclass or base class) and have child classes (subclasses) automatically receive that behavior. Each child can then add its own unique features or modify inherited behavior.

This is the IS-A relationship: a Dog IS-A Animal. A SavingsAccount IS-A BankAccount. A Manager IS-A Employee. If the IS-A sentence sounds natural, inheritance is probably the right tool.

Inheritance is one of the most powerful — and most misused — tools in OOP. Used well, it eliminates duplication and creates clean hierarchies. Used poorly, it creates rigid, fragile code that's harder to change than the problem it was trying to solve. We'll cover both.

How Inheritance Works

When a child class inherits from a parent class:

  1. The child gets everything the parent has — all attributes and methods (subject to access modifiers)
  2. The child can add new features — additional attributes and methods that only the child has
  3. The child can override behavior — replace a parent method with its own version
  4. The child can extend behavior — call the parent's method and add extra logic

Let's see this with a concrete example: an employee management system.

class Employee:
    """Base class — shared behavior for ALL employees."""

    def __init__(self, name: str, employee_id: str, base_salary: float):
        self.name = name
        self.employee_id = employee_id
        self.base_salary = base_salary

    def calculate_pay(self) -> float:
        """Default pay calculation — just the base salary."""
        return self.base_salary

    def get_role(self) -> str:
        """Returns the employee's role. Can be overridden."""
        return "Employee"

    def display_info(self) -> None:
        """Display employee details."""
        print(f"[{self.get_role()}] {self.name} (ID: {self.employee_id})")
        print(f"  Pay: ${self.calculate_pay():,.2f}")


class Manager(Employee):
    """A Manager IS-A Employee, with additional responsibilities."""

    def __init__(self, name: str, employee_id: str, base_salary: float,
                 team_size: int, bonus_rate: float = 0.15):
        # Call parent constructor to set up shared attributes
        super().__init__(name, employee_id, base_salary)
        # Add Manager-specific attributes
        self.team_size = team_size
        self.bonus_rate = bonus_rate

    def calculate_pay(self) -> float:
        """Override: managers get a bonus based on team size."""
        bonus = self.base_salary * self.bonus_rate
        return self.base_salary + bonus

    def get_role(self) -> str:
        """Override: return the specific role."""
        return "Manager"

    def approve_leave(self, employee_name: str) -> None:
        """New method — only managers can approve leave."""
        print(f"  {self.name} approved leave for {employee_name}")


class Developer(Employee):
    """A Developer IS-A Employee, with technical specialization."""

    def __init__(self, name: str, employee_id: str, base_salary: float,
                 tech_stack: list[str], is_senior: bool = False):
        super().__init__(name, employee_id, base_salary)
        self.tech_stack = tech_stack
        self.is_senior = is_senior

    def calculate_pay(self) -> float:
        """Override: seniors get a 20% premium."""
        if self.is_senior:
            return self.base_salary * 1.20
        return self.base_salary

    def get_role(self) -> str:
        return "Senior Developer" if self.is_senior else "Developer"

    def code_review(self, pr_title: str) -> None:
        """New method — only developers do code reviews."""
        print(f"  {self.name} reviewed PR: '{pr_title}'")


class Intern(Employee):
    """An Intern IS-A Employee, with a mentor assigned."""

    def __init__(self, name: str, employee_id: str, stipend: float,
                 mentor: Employee):
        super().__init__(name, employee_id, stipend)
        self.mentor = mentor

    def get_role(self) -> str:
        return "Intern"

    def display_info(self) -> None:
        """Extend: call parent's version, then add mentor info."""
        super().display_info()  # Reuse parent's logic
        print(f"  Mentor: {self.mentor.name}")


# --- Usage ---
manager = Manager("Sarah", "MGR-001", 120000, team_size=8)
dev = Developer("James", "DEV-042", 95000, ["Python", "Go"], is_senior=True)
intern = Intern("Alex", "INT-007", 30000, mentor=dev)

# All employees share the same interface:
for emp in [manager, dev, intern]:
    emp.display_info()
    print()

# Type-specific methods:
manager.approve_leave("James")
dev.code_review("Fix authentication bug")

# Output:
# [Manager] Sarah (ID: MGR-001)
#   Pay: $138,000.00
#
# [Senior Developer] James (ID: DEV-042)
#   Pay: $114,000.00
#
# [Intern] Alex (ID: INT-007)
#   Pay: $30,000.00
#   Mentor: James
#
#   Sarah approved leave for James
#   James reviewed PR: 'Fix authentication bug'
import java.util.List;

public class Employee {
    protected String name;
    protected String employeeId;
    protected double baseSalary;

    public Employee(String name, String employeeId, double baseSalary) {
        this.name = name;
        this.employeeId = employeeId;
        this.baseSalary = baseSalary;
    }

    public double calculatePay() {
        return baseSalary;
    }

    public String getRole() {
        return "Employee";
    }

    public void displayInfo() {
        System.out.printf("[%s] %s (ID: %s)%n", getRole(), name, employeeId);
        System.out.printf("  Pay: $%,.2f%n", calculatePay());
    }
}

class Manager extends Employee {
    private int teamSize;
    private double bonusRate;

    public Manager(String name, String employeeId, double baseSalary,
                   int teamSize, double bonusRate) {
        super(name, employeeId, baseSalary);
        this.teamSize = teamSize;
        this.bonusRate = bonusRate;
    }

    @Override
    public double calculatePay() {
        double bonus = baseSalary * bonusRate;
        return baseSalary + bonus;
    }

    @Override
    public String getRole() {
        return "Manager";
    }

    public void approveLeave(String employeeName) {
        System.out.println("  " + name + " approved leave for " + employeeName);
    }
}

class Developer extends Employee {
    private List<String> techStack;
    private boolean isSenior;

    public Developer(String name, String employeeId, double baseSalary,
                     List<String> techStack, boolean isSenior) {
        super(name, employeeId, baseSalary);
        this.techStack = techStack;
        this.isSenior = isSenior;
    }

    @Override
    public double calculatePay() {
        return isSenior ? baseSalary * 1.20 : baseSalary;
    }

    @Override
    public String getRole() {
        return isSenior ? "Senior Developer" : "Developer";
    }

    public void codeReview(String prTitle) {
        System.out.println("  " + name + " reviewed PR: '" + prTitle + "'");
    }
}

class Intern extends Employee {
    private Employee mentor;

    public Intern(String name, String employeeId, double stipend, Employee mentor) {
        super(name, employeeId, stipend);
        this.mentor = mentor;
    }

    @Override
    public String getRole() {
        return "Intern";
    }

    @Override
    public void displayInfo() {
        super.displayInfo(); // Reuse parent's logic
        System.out.println("  Mentor: " + mentor.name);
    }
}

class InheritanceDemo {
    public static void main(String[] args) {
        Manager manager = new Manager("Sarah", "MGR-001", 120000, 8, 0.15);
        Developer dev = new Developer("James", "DEV-042", 95000,
                List.of("Python", "Go"), true);
        Intern intern = new Intern("Alex", "INT-007", 30000, dev);

        Employee[] team = {manager, dev, intern};
        for (Employee emp : team) {
            emp.displayInfo();
            System.out.println();
        }

        manager.approveLeave("James");
        dev.codeReview("Fix authentication bug");
    }
}
Inheritance hierarchy. Base class Employee (name, employee_id, base_salary; calculate_pay, get_role, display_info) is extended by three children via IS-A. Manager (Sarah, base 20k) overrides calculate_pay (+bonus) and get_role, and adds approve_leave(). Developer (James, senior, base 5k) overrides calculate_pay (+20%) and get_role, and adds code_review(). Intern (Alex, stipend 0k) inherits calculate_pay, overrides get_role, and extends display_info via super(). Legend: inherit (use parent's as-is), override (replace parent's version), extend (super() + add more), add (new child-only method).

The super() Keyword — Connecting to the Parent

The super() keyword (Python) or super keyword (Java) is how a child class communicates with its parent:

UsePurposeExample
super().__init__(...)Call parent's constructorSet up inherited attributes
super().method()Call parent's version of a methodExtend behavior instead of replacing it

The Intern.display_info() method demonstrates extending behavior. It calls super().display_info() to get the standard output, then adds the mentor line. This is different from overriding (like Manager.calculate_pay()), which replaces the parent's logic entirely.

Rule of thumb:

  • Override when the child's behavior is fundamentally different from the parent's
  • Extend (using super()) when the child does everything the parent does, plus more

Visualization

Call a method on Sarah (Manager), James (Developer), or Alex (Intern) and watch which class actually runs it: the child's OVERRIDE, the INHERITED Employee version, an EXTEND via super(), an ADDED child-only method, or MISSING (AttributeError). The hierarchy diagram highlights the resolving class.

Types of Inheritance

TypeStructureExampleSupport
SingleA → BEmployee → ManagerBoth Python & Java
Multi-levelA → B → CAnimal → Dog → GoldenRetrieverBoth Python & Java
HierarchicalA → B, A → C, A → DEmployee → Manager, Developer, InternBoth Python & Java
MultipleA,B → CFlyable, Swimmable → DuckPython ✅, Java ❌ (use interfaces)

Java doesn't support multiple inheritance of classes because it creates the diamond problem — if two parents have the same method, which version does the child get? Java solves this with interfaces (a class can implement multiple interfaces). Python supports multiple inheritance but resolves conflicts using the Method Resolution Order (MRO).

For this course, you'll mostly see single and hierarchical inheritance. Multiple inheritance is used selectively in design patterns (we'll encounter it in Section 12 with the Mixin pattern).

Common Mistakes

Mistake 1: Inheriting When There's No IS-A Relationship

❌ Wrong: Making Stack inherit from ArrayList because a stack "uses" a list. Now Stack has .get(index) and .remove(index) methods that violate stack behavior.

✅ Right: Use composition instead: Stack has an internal list, but only exposes push(), pop(), and peek(). We'll cover composition vs. inheritance in detail in Section 4 (Relationships Between Classes).

Mistake 2: Forgetting to Call super().init()

❌ Wrong:

class Manager(Employee):
    def __init__(self, name, salary, team_size):
        self.team_size = team_size  # Forgot super()!
        # name and salary are never set → AttributeError later

✅ Right: Always call super().__init__() first to initialize inherited attributes.

Mistake 3: Deep Inheritance Hierarchies

❌ Wrong: Animal → Mammal → DomesticMammal → Pet → Dog → SmallDog → Chihuahua. Seven levels deep. Changing anything in Animal has unpredictable effects on Chihuahua.

✅ Right: Keep hierarchies shallow — 2-3 levels maximum. Prefer composition for combining behaviors. As the saying goes: "Favor composition over inheritance."

Key Takeaways

  • Inheritance lets a child class receive attributes and methods from a parent class — eliminating code duplication
  • Use inheritance for the IS-A relationship: a Manager IS-A Employee. If the sentence sounds forced, use composition instead
  • Override replaces parent behavior. Extend (using super()) reuses parent behavior and adds more. Add introduces methods unique to the child
  • Always call super().__init__() in the child's constructor to initialize inherited attributes
  • Java supports single inheritance for classes but allows implementing multiple interfaces. Python supports multiple inheritance with MRO
  • Keep hierarchies shallow (2-3 levels). Deep hierarchies create fragile, hard-to-change code
  • The most important guideline: favor composition over inheritance. We'll explain exactly when to use each in Section 4 (Relationships Between Classes)
  • In the next tutorial, we'll learn polymorphism — the ability to treat different types uniformly through a shared interface, which is inheritance's most powerful payoff