Inheritance — Code Reuse & Hierarchies
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:
- The child gets everything the parent has — all attributes and methods (subject to access modifiers)
- The child can add new features — additional attributes and methods that only the child has
- The child can override behavior — replace a parent method with its own version
- 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");
}
}
The super() Keyword — Connecting to the Parent
The super() keyword (Python) or super keyword (Java) is how a child class communicates with its parent:
| Use | Purpose | Example |
|---|---|---|
super().__init__(...) | Call parent's constructor | Set up inherited attributes |
super().method() | Call parent's version of a method | Extend 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

Types of Inheritance
| Type | Structure | Example | Support |
|---|---|---|---|
| Single | A → B | Employee → Manager | Both Python & Java |
| Multi-level | A → B → C | Animal → Dog → GoldenRetriever | Both Python & Java |
| Hierarchical | A → B, A → C, A → D | Employee → Manager, Developer, Intern | Both Python & Java |
| Multiple | A,B → C | Flyable, Swimmable → Duck | Python ✅, 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