Inheritance in OOP: Types & Interview Guide

Updated 2026-03-11

TL;DR

Inheritance allows you to create new classes based on existing ones, inheriting their attributes and methods. This promotes code reuse and establishes “is-a” relationships between classes. It’s a fundamental pillar of OOP that enables polymorphism and helps organize code into logical hierarchies.

Prerequisites: Understanding of classes and objects, familiarity with methods and attributes, basic Python syntax including the init constructor method.

After this topic: Implement class hierarchies using inheritance, override parent methods in child classes, use super() to access parent class functionality, and identify when inheritance is appropriate versus composition.

Core Concept

What is Inheritance?

Inheritance is a mechanism where a new class (called the child class, subclass, or derived class) is created from an existing class (called the parent class, superclass, or base class). The child class automatically receives all attributes and methods from the parent class and can add new ones or modify existing behavior.

Why Inheritance Matters

Inheritance solves the problem of code duplication. Instead of copying and pasting similar code across multiple classes, you define common functionality once in a parent class and let child classes inherit it. This makes your code:

  • DRY (Don’t Repeat Yourself): Common logic lives in one place
  • Maintainable: Changes to shared behavior happen in one location
  • Extensible: New variations can be added without modifying existing code
  • Organized: Related classes form logical hierarchies

The “Is-A” Relationship

Inheritance models an “is-a” relationship. A Dog “is-a” Animal. A SavingsAccount “is-a” BankAccount. If you can’t say “Child is-a Parent” naturally, inheritance might not be the right choice.

Key Inheritance Concepts

Method Overriding: Child classes can replace parent methods with their own implementation. The child’s version takes precedence.

The super() Function: Allows child classes to call parent class methods, especially useful in constructors to initialize inherited attributes before adding child-specific ones.

Method Resolution Order (MRO): Python determines which method to call when there are multiple inheritance levels. It searches the child class first, then moves up the hierarchy.

Single vs. Multiple Inheritance

Single inheritance means a class has one parent. Multiple inheritance allows a class to inherit from multiple parents simultaneously (supported in Python and C++, but not Java). Multiple inheritance can create complexity and ambiguity, so use it sparingly.

Visual Guide

Basic Inheritance Hierarchy

classDiagram
    Animal <|-- Dog
    Animal <|-- Cat
    Animal <|-- Bird
    
    class Animal {
        +name: str
        +age: int
        +eat()
        +sleep()
    }
    
    class Dog {
        +breed: str
        +bark()
        +fetch()
    }
    
    class Cat {
        +indoor: bool
        +meow()
        +scratch()
    }
    
    class Bird {
        +can_fly: bool
        +chirp()
        +fly()
    }

Inheritance creates an “is-a” relationship. Dog, Cat, and Bird all inherit from Animal, gaining its attributes and methods while adding their own specific behaviors.

Method Resolution Order (MRO)

graph TD
    A[Child Class] -->|1. Search here first| B[Method found?]
    B -->|No| C[Parent Class]
    C -->|2. Search parent| D[Method found?]
    D -->|No| E[Grandparent Class]
    E -->|3. Continue up hierarchy| F[Method found?]
    F -->|No| G[object base class]
    B -->|Yes| H[Execute method]
    D -->|Yes| H
    F -->|Yes| H
    G -->|4. AttributeError if not found| I[Raise Error]

Python searches for methods starting at the child class and moving up the inheritance chain until it finds the method or reaches the top (object class).

Inheritance vs. Composition Decision Tree

graph TD
    A[Need to share functionality?] -->|Yes| B{Is there an 'is-a' relationship?}
    A -->|No| C[No relationship needed]
    B -->|Yes| D[Use Inheritance]
    B -->|No| E{Is there a 'has-a' relationship?}
    E -->|Yes| F[Use Composition]
    E -->|No| G[Consider other patterns]
    
    D -->|Example| H[Dog is-a Animal]
    F -->|Example| I[Car has-a Engine]

Choose inheritance when there’s a true “is-a” relationship. Use composition (one class contains another) for “has-a” relationships.

Examples

Example 1: Basic Inheritance with Method Overriding

class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def speak(self):
        return "Some generic animal sound"
    
    def info(self):
        return f"{self.name} is {self.age} years old"

class Dog(Animal):  # Dog inherits from Animal
    def __init__(self, name, age, breed):
        super().__init__(name, age)  # Call parent constructor
        self.breed = breed
    
    def speak(self):  # Override parent method
        return "Woof! Woof!"
    
    def fetch(self):
        return f"{self.name} is fetching the ball!"

class Cat(Animal):
    def speak(self):  # Override parent method
        return "Meow!"
    
    def scratch(self):
        return f"{self.name} is scratching the furniture"

# Usage
dog = Dog("Buddy", 3, "Golden Retriever")
cat = Cat("Whiskers", 2)

print(dog.info())      # Inherited from Animal
print(dog.speak())     # Overridden in Dog
print(dog.fetch())     # Dog-specific method
print(dog.breed)       # Dog-specific attribute

print(cat.info())      # Inherited from Animal
print(cat.speak())     # Overridden in Cat
print(cat.scratch())   # Cat-specific method

Expected Output:

Buddy is 3 years old
Woof! Woof!
Buddy is fetching the ball!
Golden Retriever
Whiskers is 2 years old
Meow!
Whiskers is scratching the furniture

Key Points:

  • Dog(Animal) syntax means Dog inherits from Animal
  • super().__init__(name, age) calls the parent constructor to initialize inherited attributes
  • Both Dog and Cat override speak() with their own implementations
  • info() is inherited and works without modification
  • Child classes add their own unique methods (fetch(), scratch())

Java/C++ Note: In Java, use extends keyword: class Dog extends Animal. In C++, use : public Animal syntax: class Dog : public Animal.

Try it yourself: Create a Bird class that inherits from Animal, overrides speak() to return “Chirp!”, and adds a fly() method. Test with a bird instance.


Example 2: Using super() to Extend Parent Methods

class BankAccount:
    def __init__(self, account_number, balance=0):
        self.account_number = account_number
        self.balance = balance
    
    def deposit(self, amount):
        if amount > 0:
            self.balance += amount
            return f"Deposited ${amount}. New balance: ${self.balance}"
        return "Invalid deposit amount"
    
    def withdraw(self, amount):
        if 0 < amount <= self.balance:
            self.balance -= amount
            return f"Withdrew ${amount}. New balance: ${self.balance}"
        return "Insufficient funds or invalid amount"

class SavingsAccount(BankAccount):
    def __init__(self, account_number, balance=0, interest_rate=0.02):
        super().__init__(account_number, balance)
        self.interest_rate = interest_rate
    
    def apply_interest(self):
        interest = self.balance * self.interest_rate
        self.balance += interest
        return f"Interest applied: ${interest:.2f}. New balance: ${self.balance:.2f}"
    
    def withdraw(self, amount):
        # Extend parent method with additional logic
        if amount > 500:
            return "Savings account: Cannot withdraw more than $500 at once"
        return super().withdraw(amount)  # Call parent's withdraw

class CheckingAccount(BankAccount):
    def __init__(self, account_number, balance=0, overdraft_limit=100):
        super().__init__(account_number, balance)
        self.overdraft_limit = overdraft_limit
    
    def withdraw(self, amount):
        # Override with different logic
        if 0 < amount <= self.balance + self.overdraft_limit:
            self.balance -= amount
            return f"Withdrew ${amount}. New balance: ${self.balance}"
        return "Exceeds overdraft limit"

# Usage
savings = SavingsAccount("SAV001", 1000, 0.03)
checking = CheckingAccount("CHK001", 200, 150)

print(savings.deposit(500))        # Inherited method
print(savings.apply_interest())    # SavingsAccount-specific
print(savings.withdraw(600))       # Overridden with restriction
print(savings.withdraw(400))       # Calls parent's withdraw via super()

print(checking.deposit(100))       # Inherited method
print(checking.withdraw(250))      # Overridden with overdraft
print(checking.withdraw(200))      # Would exceed overdraft

Expected Output:

Deposited $500. New balance: $1500
Interest applied: $45.00. New balance: $1545.00
Savings account: Cannot withdraw more than $500 at once
Withdrew $400. New balance: $1145.0
Deposited $100. New balance: $300
Withdrew $250. New balance: $50
Exceeds overdraft limit

Key Points:

  • super().withdraw(amount) in SavingsAccount calls the parent’s withdraw method after checking the $500 limit
  • CheckingAccount completely overrides withdraw with different logic (no super() call)
  • Both child classes extend the parent constructor with additional attributes
  • Inherited methods like deposit() work unchanged in both child classes

Try it yourself: Add a transfer(amount, target_account) method to BankAccount that withdraws from one account and deposits to another. Test it with both SavingsAccount and CheckingAccount instances.


Example 3: Multi-Level Inheritance

class Vehicle:
    def __init__(self, brand, year):
        self.brand = brand
        self.year = year
    
    def start(self):
        return f"{self.brand} vehicle starting..."
    
    def stop(self):
        return f"{self.brand} vehicle stopping..."

class Car(Vehicle):
    def __init__(self, brand, year, num_doors):
        super().__init__(brand, year)
        self.num_doors = num_doors
    
    def honk(self):
        return "Beep beep!"

class ElectricCar(Car):
    def __init__(self, brand, year, num_doors, battery_capacity):
        super().__init__(brand, year, num_doors)
        self.battery_capacity = battery_capacity
        self.charge_level = 100
    
    def start(self):
        # Override grandparent's method
        if self.charge_level > 0:
            return f"{self.brand} electric motor starting silently..."
        return "Battery dead! Cannot start."
    
    def charge(self, hours):
        charge_added = min(hours * 10, 100 - self.charge_level)
        self.charge_level += charge_added
        return f"Charged for {hours} hours. Battery: {self.charge_level}%"

# Usage
tesla = ElectricCar("Tesla", 2023, 4, 75)

print(tesla.start())           # Overridden in ElectricCar
print(tesla.honk())            # Inherited from Car
print(tesla.stop())            # Inherited from Vehicle (grandparent)
print(tesla.charge(3))         # ElectricCar-specific
print(f"Doors: {tesla.num_doors}")  # From Car
print(f"Year: {tesla.year}")        # From Vehicle

# Check the Method Resolution Order
print(ElectricCar.__mro__)

Expected Output:

Tesla electric motor starting silently...
Beep beep!
Tesla vehicle stopping...
Charged for 3 hours. Battery: 100%
Doors: 4
Year: 2023
(<class '__main__.ElectricCar'>, <class '__main__.Car'>, <class '__main__.Vehicle'>, <class 'object'>)

Key Points:

  • ElectricCar inherits from Car, which inherits from Vehicle (three-level hierarchy)
  • ElectricCar can access methods from both Car and Vehicle
  • __mro__ shows the Method Resolution Order: ElectricCar → Car → Vehicle → object
  • Each super().__init__() call passes control up one level in the hierarchy

Try it yourself: Add a HybridCar class that inherits from Car and has both a battery_capacity and fuel_tank_size. Override start() to check both fuel and battery levels.


Example 4: Checking Inheritance Relationships

class Animal:
    pass

class Dog(Animal):
    pass

class Cat(Animal):
    pass

class GoldenRetriever(Dog):
    pass

# Create instances
buddy = GoldenRetriever()
whiskers = Cat()

# isinstance() checks if an object is an instance of a class or its parents
print(isinstance(buddy, GoldenRetriever))  # True
print(isinstance(buddy, Dog))              # True
print(isinstance(buddy, Animal))           # True
print(isinstance(buddy, Cat))              # False

# issubclass() checks if a class inherits from another
print(issubclass(GoldenRetriever, Dog))    # True
print(issubclass(GoldenRetriever, Animal)) # True
print(issubclass(Dog, Animal))             # True
print(issubclass(Dog, Cat))                # False

# type() returns the exact class (not parents)
print(type(buddy))                         # <class '__main__.GoldenRetriever'>
print(type(buddy) == GoldenRetriever)      # True
print(type(buddy) == Dog)                  # False (even though buddy is-a Dog)

Expected Output:

True
True
True
False
True
True
True
False
<class '__main__.GoldenRetriever'>
True
False

Key Points:

  • Use isinstance(obj, Class) to check if an object is an instance of a class or any of its parent classes
  • Use issubclass(ChildClass, ParentClass) to check inheritance relationships between classes
  • type() returns the exact class, not considering inheritance
  • In interviews, prefer isinstance() over type() for type checking because it respects inheritance

Try it yourself: Create a function describe_animal(animal) that uses isinstance() to print different messages for Dog, Cat, or generic Animal instances.

Common Mistakes

1. Forgetting to Call super().init()

# WRONG
class Dog(Animal):
    def __init__(self, name, breed):
        # Forgot to call super().__init__()
        self.breed = breed

dog = Dog("Buddy", "Labrador")
print(dog.name)  # AttributeError: 'Dog' object has no attribute 'name'

Why it’s wrong: The parent class’s __init__ never runs, so inherited attributes like name and age are never created.

Fix: Always call super().__init__() with the appropriate arguments before initializing child-specific attributes.

# CORRECT
class Dog(Animal):
    def __init__(self, name, age, breed):
        super().__init__(name, age)  # Initialize parent attributes first
        self.breed = breed

2. Overusing Inheritance Instead of Composition

# WRONG - Car is NOT an Engine
class Car(Engine):
    def __init__(self, horsepower, cylinders):
        super().__init__(horsepower, cylinders)

Why it’s wrong: A Car “has-a” Engine, not “is-a” Engine. This violates the “is-a” principle and creates confusing hierarchies.

Fix: Use composition when there’s a “has-a” relationship.

# CORRECT - Composition
class Engine:
    def __init__(self, horsepower, cylinders):
        self.horsepower = horsepower
        self.cylinders = cylinders

class Car:
    def __init__(self, engine):
        self.engine = engine  # Car HAS-A Engine

engine = Engine(300, 6)
car = Car(engine)

Rule of thumb: If you can’t naturally say “Child is-a Parent,” use composition instead.


3. Creating Deep Inheritance Hierarchies

# PROBLEMATIC - Too many levels
class LivingThing:
    pass

class Animal(LivingThing):
    pass

class Mammal(Animal):
    pass

class Carnivore(Mammal):
    pass

class Canine(Carnivore):
    pass

class Dog(Canine):
    pass

class GoldenRetriever(Dog):
    pass

Why it’s problematic: Deep hierarchies (more than 3-4 levels) become hard to understand, maintain, and debug. Changes at the top ripple down unpredictably.

Fix: Keep hierarchies shallow. Favor composition and interfaces/protocols for flexibility.

# BETTER - Flatter hierarchy
class Animal:
    pass

class Dog(Animal):
    def __init__(self, breed):
        self.breed = breed

dog = Dog("Golden Retriever")  # Breed is data, not a class

4. Shadowing Parent Attributes Unintentionally

class Parent:
    def __init__(self):
        self.value = 10

class Child(Parent):
    def __init__(self):
        super().__init__()
        self.value = 20  # Shadows parent's value
    
    def get_parent_value(self):
        # No way to access parent's original value!
        return self.value  # Returns 20, not 10

Why it’s problematic: Once you reassign self.value, the parent’s original value is lost. This can cause confusion.

Fix: Use different attribute names or be intentional about overriding.

class Child(Parent):
    def __init__(self):
        super().__init__()
        self.child_value = 20  # Different name
        # self.value still holds parent's 10

5. Using type() Instead of isinstance() for Type Checking

class Animal:
    pass

class Dog(Animal):
    pass

dog = Dog()

# WRONG - Doesn't respect inheritance
if type(dog) == Animal:
    print("It's an animal")  # This won't print!

# CORRECT - Respects inheritance
if isinstance(dog, Animal):
    print("It's an animal")  # This prints!

Why it’s wrong: type() checks for exact class match, ignoring inheritance. A Dog is an Animal, but type(dog) == Animal returns False.

Fix: Use isinstance() for type checking in polymorphic code.

Interview note: Interviewers often test whether you understand the difference between type() and isinstance(). Always prefer isinstance() unless you specifically need the exact class.

Interview Tips

1. Explain the “Is-A” vs. “Has-A” Distinction Clearly

Interviewers frequently ask when to use inheritance versus composition. Have a crisp answer ready:

Your response: “I use inheritance for ‘is-a’ relationships where the child class is a specialized version of the parent. For example, a Dog is-a Animal. I use composition for ‘has-a’ relationships where one class contains another. For example, a Car has-a Engine. The Liskov Substitution Principle guides this: if I can’t substitute a child instance wherever a parent instance is expected, inheritance is wrong.”

Follow-up they might ask: “Why prefer composition over inheritance?” Answer: “Composition is more flexible. It avoids tight coupling and deep hierarchies. I can change behavior at runtime by swapping components, which inheritance doesn’t allow.”


2. Be Ready to Code Inheritance On the Spot

Common interview question: “Design a class hierarchy for [vehicles/employees/shapes/accounts].”

Your approach:

  1. Identify the base class with common attributes/methods
  2. Create 2-3 child classes that specialize the base
  3. Show method overriding in at least one child
  4. Use super() correctly in constructors
  5. Demonstrate polymorphism by treating different children as the parent type

Example prompt: “Design classes for different types of employees.”

class Employee:
    def __init__(self, name, id, base_salary):
        self.name = name
        self.id = id
        self.base_salary = base_salary
    
    def calculate_pay(self):
        return self.base_salary

class Manager(Employee):
    def __init__(self, name, id, base_salary, bonus):
        super().__init__(name, id, base_salary)
        self.bonus = bonus
    
    def calculate_pay(self):
        return self.base_salary + self.bonus

class SalesRep(Employee):
    def __init__(self, name, id, base_salary, commission_rate):
        super().__init__(name, id, base_salary)
        self.commission_rate = commission_rate
        self.sales = 0
    
    def calculate_pay(self):
        return self.base_salary + (self.sales * self.commission_rate)

# Polymorphism in action
employees = [
    Manager("Alice", 1, 80000, 20000),
    SalesRep("Bob", 2, 50000, 0.1)
]

for emp in employees:
    print(f"{emp.name}: ${emp.calculate_pay()}")  # Same interface, different behavior

This demonstrates you understand inheritance, overriding, and polymorphism.


3. Know When NOT to Use Inheritance

Interviewers test your judgment. Be ready to critique inheritance:

Red flags for inheritance:

  • The relationship isn’t truly “is-a” (use composition)
  • You need multiple unrelated behaviors (use interfaces/mixins)
  • The hierarchy would be more than 3-4 levels deep (flatten it)
  • Child classes would need to override most parent methods (wrong abstraction)

Example answer: “If I’m designing a Duck class that can swim and fly, I wouldn’t create a hierarchy like Bird → FlyingBird → SwimmingBird → Duck. That’s fragile because not all birds fly or swim. Instead, I’d use composition with Flyable and Swimmable components, or use interfaces/protocols.”


4. Discuss the Liskov Substitution Principle (LSP)

This is a favorite interview topic. LSP states: “Objects of a subclass should be replaceable with objects of the superclass without breaking the program.”

Classic violation example:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height

class Square(Rectangle):  # Violates LSP!
    def __init__(self, side):
        super().__init__(side, side)
    
    def set_width(self, width):
        self.width = width
        self.height = width  # Breaks Rectangle's assumption

Why it violates LSP: A Square can’t truly substitute for a Rectangle because changing width also changes height, which Rectangle users don’t expect.

Your answer: “Square shouldn’t inherit from Rectangle because their behaviors differ fundamentally. A better design uses a common Shape interface or keeps them separate.”


5. Explain Method Resolution Order (MRO)

For Python interviews, know how MRO works, especially with multiple inheritance:

Question: “What happens when a class inherits from multiple parents with the same method?”

Your answer: “Python uses C3 linearization to determine MRO. It searches left-to-right, depth-first, but ensures each class appears before its parents. You can check the order with ClassName.__mro__ or ClassName.mro(). In practice, I avoid complex multiple inheritance and use mixins carefully to prevent ambiguity.”

Example:

class A:
    def method(self):
        return "A"

class B(A):
    def method(self):
        return "B"

class C(A):
    def method(self):
        return "C"

class D(B, C):  # Multiple inheritance
    pass

d = D()
print(d.method())  # Prints "B" (B comes before C in MRO)
print(D.__mro__)   # Shows: D → B → C → A → object

6. Practice Refactoring Code to Use Inheritance

Interviewers may give you duplicate code and ask you to refactor:

Before:

class Circle:
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return 3.14 * self.radius ** 2
    
    def perimeter(self):
        return 2 * 3.14 * self.radius

class Square:
    def __init__(self, side):
        self.side = side
    
    def area(self):
        return self.side ** 2
    
    def perimeter(self):
        return 4 * self.side

After (with inheritance):

class Shape:
    def describe(self):
        return f"Area: {self.area()}, Perimeter: {self.perimeter()}"
    
    def area(self):
        raise NotImplementedError("Subclass must implement area()")
    
    def perimeter(self):
        raise NotImplementedError("Subclass must implement perimeter()")

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return 3.14 * self.radius ** 2
    
    def perimeter(self):
        return 2 * 3.14 * self.radius

class Square(Shape):
    def __init__(self, side):
        self.side = side
    
    def area(self):
        return self.side ** 2
    
    def perimeter(self):
        return 4 * self.side

This shows you can identify common patterns and extract them into a base class.


7. Know the Syntax Differences Across Languages

If interviewing for Java or C++ roles, know the syntax:

Python: class Dog(Animal): Java: class Dog extends Animal {} C++: class Dog : public Animal {}

Python: super().__init__(args) Java: super(args); (in constructor) C++: : Animal(args) (initializer list)

Being fluent in multiple syntaxes shows versatility.

Key Takeaways

  • Inheritance creates “is-a” relationships where child classes inherit attributes and methods from parent classes, promoting code reuse and establishing logical hierarchies.
  • Use super() to call parent methods, especially in constructors, to properly initialize inherited attributes before adding child-specific ones.
  • Method overriding allows child classes to replace parent behavior while still accessing the original via super() if needed.
  • Prefer composition over inheritance for “has-a” relationships and keep hierarchies shallow (2-3 levels max) to maintain flexibility and avoid tight coupling.
  • Use isinstance() for type checking to respect inheritance relationships, and understand that the Liskov Substitution Principle requires child objects to be substitutable for parent objects without breaking functionality.