Inheritance & Polymorphism

8 min read

Reading Progress0%
CS Fundamentals Index
Tier 1 -- Foundations
Tier 2 -- Core Concepts
Tier 3 -- Debugging & Tradeoffs
CS Fundamentals Index
Tier 1 -- Foundations
Tier 2 -- Core Concepts
Tier 3 -- Debugging & Tradeoffs

Inheritance & Polymorphism

1. What Is It?

lets one reuse and extend another. You declare Dog extends Animal (Java) or class Dog(Animal) (Python), and Dog automatically gets Animal's fields and methods — plus whatever Dog adds or overrides. The goal is reuse without copy-paste, and a shared vocabulary for related types.

is the payoff: code written against the parent type works unchanged when you pass a child type. A function that takes Animal can receive any Dog, Cat, or Sparrow, and calling animal.speak() runs the right version for each. Without , every new type forces you to open up and edit every function that handles it — the classic cascade of if isinstance(...) or switch(type) blocks.

QUICK CHECK

A backend service handles payments and currently has a processPayment(CreditCardPayment payment) function. The team needs to add support for PayPal and crypto payments. Without polymorphism, what is the most likely maintenance problem they will face?

Choose one answer

2. How It Works

When Dog inherits from Animal, a Dog has everything an Animal has. The parent's fields and methods are present in the child, and the child can:

  • Add new fields and methods
  • Override a parent method to change its behavior
  • Call the parent's version via super() / super.method() / Parent::method()
# Python
class Animal:
    def __init__(self, name):
        self.name = name
    def speak(self):
        return "..."

class Dog(Animal):
    def speak(self):                      # override
        return f"{self.name} says woof"

class Puppy(Dog):
    def speak(self):
        return super().speak() + " (small)"  # reuse parent

print(Puppy("Rex").speak())               # Rex says woof (small)
// Java
public class Animal {
    protected String name;
    public Animal(String name) { this.name = name; }
    public String speak() { return "..."; }
}

public class Dog extends Animal {
    public Dog(String name) { super(name); }
    @Override
    public String speak() { return name + " says woof"; }
}
// C++
class Animal {
public:
    Animal(std::string name) : name_(std::move(name)) {}
    virtual std::string speak() const { return "..."; }     // 'virtual' = overridable
    virtual ~Animal() = default;                            // virtual destructor!
protected:
    std::string name_;
};

class Dog : public Animal {
public:
    Dog(std::string name) : Animal(std::move(name)) {}
    std::string speak() const override { return name_ + " says woof"; }
};

Polymorphism: one call, many behaviors

# Python — duck typing; no declaration needed
def describe(animal):
    print(animal.speak())

describe(Dog("Rex"))     # Rex says woof
describe(Puppy("Max"))   # Max says woof (small)
// Java — variable type is Animal, runtime picks the right method
Animal a = new Dog("Rex");
System.out.println(a.speak());   // "Rex says woof" — dynamic dispatch
// C++ — dynamic dispatch requires a pointer/reference AND 'virtual'
Animal* a = new Dog("Rex");
std::cout << a->speak();         // "Rex says woof"
delete a;

Animal b = Dog("Rex");           // OBJECT SLICING — b is now just an Animal
std::cout << b.speak();          // "..." — Dog part was sliced off

The vtable — how dynamic dispatch works

When a method is polymorphic, the compiler can't decide at compile time which function to call — it depends on the 's actual type at runtime. Languages implement this with a vtable (virtual method table): each has a table of function pointers, and each object carries a hidden pointer to its 's table. A call like a.speak() becomes "look up speak in a's vtable, call that."

  • Python / Java: every method is dispatched through this mechanism by default.
  • C++: only methods marked virtual are. Non-virtual calls are resolved at compile time and are faster — but won't do .
QUICK CHECK

A backend service stores a collection of shape objects in C++. A developer writes the following code to compute the total area:

std::vector<Shape> shapes;
shapes.push_back(Circle(5.0));
shapes.push_back(Rectangle(3.0, 4.0));

for (Shape s : shapes) {
    std::cout << s.area() << "\n";
}
Despite Circle and Rectangle overriding area(), every call prints the base Shape::area() result. What is the most likely cause?

Choose one answer

3. What You Actually Need to Know

  • Overriding vs. overloading are different. Overriding replaces a parent's method in a child (runtime dispatch). Overloading means multiple methods with the same name but different parameters in the same (compile-time; Python effectively doesn't have it).
  • Use @Override (Java) or override (C++11+). These make the compiler check you actually overrode something — catches typos like equals() vs. equal() that silently create a new method.
  • C++: mark destructors virtual in any class you might inherit from. Otherwise delete parent_ptr won't run the child's destructor — resource leak.
  • C++: beware object slicing. Assigning a Dog to an Animal (by value) copies only the Animal part. Use pointers or references for .
  • Calling parent methodssuper().method() (Python), super.method() (Java), Parent::method() (C++). Common in __init__ / constructors so the parent initializes its own fields.
  • Debugging clues:
    • "The parent's method ran when I expected the child's" — in C++, the method isn't virtual, or you're calling by value (slicing). In Java, you declared it static or final. In Python, check your class hierarchy / MRO.
    • NoSuchMethodError after refactoring (Java) — a caller was compiled against an older class version; the method signature changed.
    • Infinite in __init__ (Python) — you typed self.__init__(...) instead of super().__init__(...).

Abstract classes and interfaces

Sometimes the parent is never meant to be instantiated — it just defines a contract. That's an , and a method with no body is an abstract method.

# Python
from abc import ABC, abstractmethod
class Shape(ABC):
    @abstractmethod
    def area(self): ...
# Shape()  → TypeError: Can't instantiate abstract class
// Java
public abstract class Shape {
    public abstract double area();   // subclasses MUST implement
}
// Or just declare a contract with no state:
public interface Drawable {
    void draw();
}
// C++ — pure virtual function makes the class abstract
class Shape {
public:
    virtual double area() const = 0;
    virtual ~Shape() = default;
};
QUICK CHECK

A C++ backend service manages database connections through a ConnectionPool base class, with a PostgresPool subclass that allocates additional resources in its constructor. The service stores pool objects via ConnectionPool* pointers and calls delete on them at shutdown. A developer notices that PostgresPool's destructor never runs, causing resource leaks. What is the most likely cause?

Choose one answer

4. Language Differences

AspectPythonJavaC++
Inherit syntaxclass B(A):class B extends Aclass B : public A
Multiple inheritanceYes — method lookup order is deterministic (C3 linearization algorithm)No (only multiple interfaces)Yes (diamond problem possible)
Call parent methodsuper().foo()super.foo()A::foo()
Default dispatchAlways dynamicAlways dynamic (unless final/static/private)Static unless virtual
Override keywordNone (duck typed)@Override (optional but recommended)override (C++11+)
Prevent inheritanceConvention onlyfinal classfinal (C++11+)
Abstract classABC + @abstractmethodabstract classclass with = 0 method
InterfaceProtocols (3.8+) / duck typinginterfaceAbstract class with only pure virtuals

Python's MRO (Method Resolution Order) is worth a glance: D.__mro__ shows exactly which will be checked for a method, in order. For simple hierarchies it's obvious; for diamond it's the rule. Python computes MRO using an algorithm called C3 linearization — you rarely need the details, just know that __mro__ is the source of truth when you're unsure which method will win.

C++'s diamond problem — D inherits from both B and C, both of which inherit from A — is solved with virtual ( B : virtual public A), which ensures one shared A sub- rather than two.

QUICK CHECK

A Python backend service has a class hierarchy where APIHandler inherits from both AuthMixin and LoggingMixin, and both mixins themselves inherit from a shared BaseHandler class. A developer wants to know which version of BaseHandler.setup() will be called when APIHandler invokes it. What is the most reliable way to determine this in Python?

Choose one answer

5. Tradeoffs & Decisions

  • vs. . couples child to parent forever — changes to the parent ripple into every descendant. (holding another as a field and delegating to it) is looser and easier to change. Default to composition; reach for inheritance when the relationship is genuinely "is-a" and you need .
  • Deep hierarchies hurt. More than two or three levels of inheritance tends to be a warning sign. Each level adds indirection and hidden behavior. Flatten with composition.
  • vs. (Java / C++). An declares a contract with no state. An can declare a contract and provide shared implementation. Choose interface when you just need the shape; abstract class when subclasses share real code.
  • virtual in C++ is a commitment. Adding virtual later to a you already released changes the 's memory layout (the vtable pointer), which forces every user of the to recompile — an "ABI break" (Application Binary Interface — the layout and calling conventions that compiled code depends on). Decide up front: is this class meant to be a polymorphic base?
  • Template Method vs. Strategy. Both enable "variation in one step of a fixed algorithm." Template Method uses inheritance (override the step). Strategy uses composition (inject an object with that step). Strategy is usually more flexible.

If you find yourself about to override a concrete parent method to subtly change its behavior, pause: you're probably making the parent's invariants harder to reason about. Consider holding the object instead (composition) or promoting the varying step to a strategy.

QUICK CHECK

Your team maintains a published C++ library used by dozens of downstream services. You now want to allow subclasses to override a key method in one of the library's core classes, but that method is currently non-virtual. What is the primary risk of adding virtual to that method in a new release?

Choose one answer

6. Interview Cheat Sheet

  • lets a reuse and extend another; lets code written against the parent type work with any child type.
  • Dynamic dispatch — the actual method called is chosen at runtime based on the 's real , via a vtable.
  • Python and Java dispatch all instance methods dynamically by default. C++ requires virtual; non-virtual calls are resolved at compile time.
  • Override vs. overload — override = same signature, different class (runtime); overload = same name, different parameters, same class (compile-time).
  • Prefer over unless you have a genuine "is-a" relationship and need .

Follow-ups:

  • "What's a vtable?" — A per-class table of function pointers. Each polymorphic holds a hidden pointer to its class's vtable; calling a virtual method is an indirect call through that table.
  • "Why does C++ have virtual but Java doesn't?" — Java assumes dynamic dispatch everywhere (pay the cost by default); C++ lets you opt in, so non-virtual calls are as fast as regular function calls.
  • "What's object slicing?" — C++-specific: assigning a derived object to a base-type by value copies only the base part. Use pointers or references for polymorphism.
  • "When would you use over inheritance?" — Almost always. Pick inheritance only when you truly have an "is-a" relationship and need the substitutability for polymorphism.
Glossary History

Click dotted jargon to save explanations here.

Glossary History

Click dotted jargon to save explanations here.