Polymorphism in OOP: Runtime vs Compile-Time

Updated 2026-03-11

TL;DR

Polymorphism allows objects of different types to be treated through a common interface while maintaining their unique behaviors. It comes in two main forms: compile-time (method overloading) and runtime (method overriding), enabling flexible and extensible code design.

Prerequisites: Understanding of classes and objects, inheritance relationships, method definitions, and basic type systems. Familiarity with parent-child class relationships and the concept of interfaces or abstract classes.

After this topic: Implement both compile-time and runtime polymorphism in your code, distinguish between method overloading and overriding, design class hierarchies that leverage polymorphic behavior, and explain how polymorphism enables code flexibility in technical interviews.

Core Concept

What is Polymorphism?

Polymorphism (Greek: “many forms”) is the ability of different objects to respond to the same message or method call in their own unique way. It’s one of the four pillars of OOP, enabling you to write code that works with a general interface while the actual implementation varies based on the object’s type.

Why Polymorphism Matters

Polymorphism solves a fundamental problem: how do you write flexible code that works with multiple types without knowing their exact implementation details? Instead of writing separate functions for each type, you define a common interface and let each type implement it differently.

This leads to:

  • Extensibility: Add new types without changing existing code
  • Maintainability: Changes to one implementation don’t affect others
  • Flexibility: Write generic algorithms that work with many types

Two Types of Polymorphism

Compile-Time Polymorphism (Static)

Method Overloading occurs when multiple methods share the same name but differ in their parameter lists (number, type, or order). The compiler determines which method to call based on the arguments provided.

Runtime Polymorphism (Dynamic)

Method Overriding happens when a child class provides a specific implementation of a method already defined in its parent class. The decision of which method to execute is made at runtime based on the actual object type, not the reference type.

The Substitution Principle

Polymorphism relies on the Liskov Substitution Principle: objects of a parent class should be replaceable with objects of a child class without breaking the program. This means a child class must honor the contract established by its parent.

How It Works Under the Hood

When you call a method on an object through a parent class reference, the language runtime uses a virtual method table (vtable) to look up the actual method implementation. This indirection enables dynamic dispatch — calling the correct method based on the object’s actual type at runtime.

Visual Guide

Polymorphism Type Hierarchy

graph TD
    A[Polymorphism] --> B[Compile-Time/Static]
    A --> C[Runtime/Dynamic]
    B --> D[Method Overloading]
    B --> E[Operator Overloading]
    C --> F[Method Overriding]
    C --> G[Interface Implementation]
    
    style A fill:#e1f5ff
    style B fill:#fff4e1
    style C fill:#ffe1f5

The two main categories of polymorphism and their common implementations

Runtime Polymorphism in Action

classDiagram
    class Animal {
        +make_sound()
    }
    class Dog {
        +make_sound()
    }
    class Cat {
        +make_sound()
    }
    class Bird {
        +make_sound()
    }
    
    Animal <|-- Dog
    Animal <|-- Cat
    Animal <|-- Bird
    
    note for Animal "Parent defines interface"
    note for Dog "Each child implements differently"

Multiple classes inherit from Animal and override make_sound() with their own implementation

Method Resolution at Runtime

sequenceDiagram
    participant Code
    participant Reference
    participant VTable
    participant ActualObject
    
    Code->>Reference: animal.make_sound()
    Note over Reference: Reference type: Animal
    Reference->>VTable: Lookup method
    VTable->>ActualObject: Find actual type (Dog)
    ActualObject->>Code: Execute Dog.make_sound()
    Note over Code: Returns "Woof!"

How the runtime resolves which method to call based on the actual object type

Examples

Example 1: Runtime Polymorphism with Method Overriding

class Animal:
    def make_sound(self):
        return "Some generic sound"
    
    def describe(self):
        return f"I am an animal and I say: {self.make_sound()}"

class Dog(Animal):
    def make_sound(self):
        return "Woof!"

class Cat(Animal):
    def make_sound(self):
        return "Meow!"

class Bird(Animal):
    def make_sound(self):
        return "Chirp!"

# Polymorphism in action
def animal_concert(animals):
    for animal in animals:
        print(animal.make_sound())

# All references are type Animal, but actual objects differ
animals = [Dog(), Cat(), Bird(), Animal()]
animal_concert(animals)

# Expected Output:
# Woof!
# Meow!
# Chirp!
# Some generic sound

# The describe method in parent uses polymorphic make_sound
dog = Dog()
print(dog.describe())
# Expected Output: I am an animal and I say: Woof!

Key Points:

  • The animal_concert function works with the Animal interface
  • Each object responds to make_sound() with its own implementation
  • The parent’s describe() method automatically uses the child’s overridden method
  • This is dynamic dispatch — the method called is determined at runtime

Java/C++ Note: In Java, all non-static, non-final, non-private methods are virtual by default. In C++, you must explicitly mark methods as virtual in the base class for polymorphism to work.

Try it yourself: Add a Fish class that returns “Blub!” and include it in the concert.


Example 2: Compile-Time Polymorphism with Method Overloading

class Calculator:
    def add(self, a, b, c=0):
        """Python uses default arguments instead of true overloading"""
        return a + b + c
    
    def process(self, data):
        """Single dispatch based on type checking"""
        if isinstance(data, int):
            return data * 2
        elif isinstance(data, str):
            return data.upper()
        elif isinstance(data, list):
            return sum(data)
        else:
            return None

calc = Calculator()
print(calc.add(5, 3))        # Expected Output: 8
print(calc.add(5, 3, 2))     # Expected Output: 10

print(calc.process(10))           # Expected Output: 20
print(calc.process("hello"))      # Expected Output: HELLO
print(calc.process([1, 2, 3]))    # Expected Output: 6

Python Note: Python doesn’t support true method overloading (multiple methods with the same name). Instead, use default arguments or type checking. For true single dispatch, use functools.singledispatch.

Java Example (True Overloading):

class Calculator {
    // These are different methods determined at compile time
    public int add(int a, int b) {
        return a + b;
    }
    
    public int add(int a, int b, int c) {
        return a + b + c;
    }
    
    public double add(double a, double b) {
        return a + b;
    }
}

Calculator calc = new Calculator();
System.out.println(calc.add(5, 3));        // Calls add(int, int) -> 8
System.out.println(calc.add(5, 3, 2));     // Calls add(int, int, int) -> 10
System.out.println(calc.add(5.5, 3.2));    // Calls add(double, double) -> 8.7

Key Difference: In Java/C++, the compiler selects the correct method at compile time based on the method signature. In Python, you have one method that handles multiple cases at runtime.

Try it yourself: Create a multiply method that works with 2 or 3 numbers.


Example 3: Polymorphism with Abstract Base Classes

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        """Must be implemented by all subclasses"""
        pass
    
    @abstractmethod
    def perimeter(self):
        pass
    
    def describe(self):
        return f"Area: {self.area()}, Perimeter: {self.perimeter()}"

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

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

# Polymorphic function
def print_shape_info(shapes):
    total_area = 0
    for shape in shapes:
        print(shape.describe())
        total_area += shape.area()
    print(f"Total area: {total_area}")

shapes = [
    Rectangle(5, 3),
    Circle(4),
    Rectangle(2, 8)
]

print_shape_info(shapes)

# Expected Output:
# Area: 15, Perimeter: 16
# Area: 50.26544, Perimeter: 25.13272
# Area: 16, Perimeter: 20
# Total area: 81.26544

Key Points:

  • Shape is an abstract base class that defines the interface
  • You cannot instantiate Shape directly — it forces subclasses to implement required methods
  • print_shape_info works with any Shape subclass without knowing the specific type
  • The describe() method in the parent automatically uses the child’s implementation

Try it yourself: Add a Triangle class with base and height attributes.


Example 4: Duck Typing (Python’s Dynamic Polymorphism)

class Dog:
    def speak(self):
        return "Woof!"

class Cat:
    def speak(self):
        return "Meow!"

class Person:
    def speak(self):
        return "Hello!"

class Car:
    def honk(self):  # Different method name!
        return "Beep!"

# No inheritance required - just needs a speak() method
def make_it_speak(thing):
    return thing.speak()

print(make_it_speak(Dog()))     # Expected Output: Woof!
print(make_it_speak(Cat()))     # Expected Output: Meow!
print(make_it_speak(Person()))  # Expected Output: Hello!

# This will raise AttributeError
try:
    print(make_it_speak(Car()))
except AttributeError as e:
    print(f"Error: {e}")  # Expected Output: Error: 'Car' object has no attribute 'speak'

Duck Typing: “If it walks like a duck and quacks like a duck, it’s a duck.” Python doesn’t check types — it just tries to call the method. If the object has the method, it works.

Advantage: Maximum flexibility — no need for inheritance hierarchies.

Disadvantage: Errors appear at runtime, not compile time.

Try it yourself: Create a Robot class with a speak() method and test it with make_it_speak().

Common Mistakes

1. Confusing Overloading with Overriding

Mistake: Thinking Python supports method overloading like Java/C++.

# This does NOT create two methods - the second replaces the first!
class Calculator:
    def add(self, a, b):
        return a + b
    
    def add(self, a, b, c):  # This overwrites the previous add
        return a + b + c

calc = Calculator()
print(calc.add(5, 3))  # TypeError: add() missing 1 required positional argument: 'c'

Fix: Use default arguments or *args:

class Calculator:
    def add(self, a, b, c=0):
        return a + b + c

2. Forgetting to Call Parent Constructor in Override

Mistake: Overriding __init__ without calling the parent’s initialization.

class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, breed):
        self.breed = breed  # Forgot to initialize name!

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

Fix: Always call super().__init__():

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

3. Breaking the Parent’s Contract

Mistake: Changing method signatures or return types in overridden methods.

class Animal:
    def make_sound(self):
        return "Some sound"  # Returns a string

class Dog(Animal):
    def make_sound(self, volume):  # Changed signature - requires argument!
        return "Woof!" * volume

# This breaks polymorphism
animals = [Animal(), Dog()]
for animal in animals:
    print(animal.make_sound())  # TypeError on Dog - missing 'volume'

Fix: Keep the same signature. Use optional parameters if needed:

class Dog(Animal):
    def make_sound(self, volume=1):
        return "Woof!" * volume

4. Using Type Checking Instead of Polymorphism

Mistake: Checking object types instead of relying on polymorphic behavior.

# Bad - defeats the purpose of polymorphism
def process_animal(animal):
    if isinstance(animal, Dog):
        return "Woof!"
    elif isinstance(animal, Cat):
        return "Meow!"
    elif isinstance(animal, Bird):
        return "Chirp!"
    # What if we add more animals? Must modify this function!

Fix: Use polymorphism:

# Good - extensible without modification
def process_animal(animal):
    return animal.make_sound()  # Each class implements its own version

5. Not Marking Methods as Virtual in C++

Mistake (C++ specific): Forgetting the virtual keyword prevents polymorphism.

// C++ - without virtual, no polymorphism!
class Animal {
public:
    void make_sound() {  // Not virtual!
        cout << "Generic sound" << endl;
    }
};

class Dog : public Animal {
public:
    void make_sound() {
        cout << "Woof!" << endl;
    }
};

Animal* animal = new Dog();
animal->make_sound();  // Prints "Generic sound" - not polymorphic!

Fix: Use virtual keyword:

class Animal {
public:
    virtual void make_sound() {  // Now it's polymorphic
        cout << "Generic sound" << endl;
    }
};

Python Note: Python methods are virtual by default — you don’t need special keywords.

Interview Tips

1. Explain the Difference Between Overloading and Overriding

Interviewer might ask: “What’s the difference between method overloading and method overriding?”

Strong Answer Structure:

  • Overloading (compile-time): Same method name, different parameters, resolved at compile time
  • Overriding (runtime): Same method signature, different implementation in child class, resolved at runtime
  • Mention that Python doesn’t support true overloading

Example to mention: “In Java, I can have add(int, int) and add(double, double) — that’s overloading. But if a Dog class overrides Animal’s make_sound(), that’s overriding.”


2. Demonstrate Polymorphism with Code

Interviewer might ask: “Show me how you’d use polymorphism to process different payment methods.”

Approach:

  1. Create an abstract base class or interface (PaymentMethod)
  2. Define common method(s) (process_payment)
  3. Implement concrete classes (CreditCard, PayPal, Bitcoin)
  4. Write a function that accepts the base type
class PaymentMethod(ABC):
    @abstractmethod
    def process_payment(self, amount):
        pass

class CreditCard(PaymentMethod):
    def process_payment(self, amount):
        return f"Charged ${amount} to credit card"

class PayPal(PaymentMethod):
    def process_payment(self, amount):
        return f"Sent ${amount} via PayPal"

def checkout(payment_method, amount):
    return payment_method.process_payment(amount)

Why this impresses: Shows you understand abstraction, extensibility, and the Open/Closed Principle.


3. Discuss Real-World Benefits

Interviewer might ask: “Why is polymorphism important in real applications?”

Strong Answer Points:

  • Extensibility: Add new types without modifying existing code (Open/Closed Principle)
  • Maintainability: Changes isolated to specific implementations
  • Testing: Mock objects can substitute real ones with same interface
  • Plugin architectures: Load different implementations dynamically

Concrete Example: “In a logging system, I can have FileLogger, DatabaseLogger, and CloudLogger all implementing a Logger interface. The application code doesn’t need to know which logger is being used — it just calls log(message). If we need to add SlackLogger later, we don’t touch the application code.”


4. Know the Limitations and Trade-offs

Interviewer might ask: “What are the downsides of polymorphism?”

Be ready to discuss:

  • Performance: Virtual method calls have slight overhead (vtable lookup)
  • Complexity: Can make code harder to trace and debug
  • Type safety: In dynamically typed languages, errors appear at runtime
  • Overuse: Not every class hierarchy needs polymorphism

Balanced response: “Polymorphism adds a small runtime cost due to dynamic dispatch, but the benefits in code flexibility and maintainability usually outweigh this. However, I avoid creating deep inheritance hierarchies — composition is often better.”


5. Connect to Design Patterns

Interviewer might ask: “Which design patterns rely on polymorphism?”

Key Patterns to Mention:

  • Strategy Pattern: Different algorithms with same interface
  • Factory Pattern: Return different types through common interface
  • Template Method: Parent defines algorithm structure, children implement steps
  • Observer Pattern: Different observers respond to same event

Example: “The Strategy pattern is pure polymorphism — you have a context that uses a strategy interface, and you can swap in different concrete strategies at runtime. For example, different sorting algorithms all implementing a sort() method.”


6. Language-Specific Knowledge

Be prepared to discuss:

  • Python: Duck typing, abstract base classes, @abstractmethod
  • Java: Interfaces vs abstract classes, default methods in interfaces
  • C++: Virtual functions, pure virtual functions, virtual destructors

Pro tip: If asked about a language you’re less familiar with, acknowledge it and relate to what you know: “I haven’t used C++ recently, but I understand it requires the virtual keyword for polymorphism, whereas Python methods are virtual by default.”


7. Whiteboard Exercise Preparation

Common interview question: “Design a shape hierarchy with polymorphic area calculation.”

Quick mental checklist:

  1. Start with abstract base class
  2. Define abstract method(s)
  3. Create 2-3 concrete implementations
  4. Write a function that uses the base type
  5. Demonstrate it works with different types

Time-saving tip: Practice writing this pattern quickly. It appears in many interview questions disguised as different domains (vehicles, employees, notifications, etc.).

Key Takeaways

  • Polymorphism enables one interface, many implementations — write code that works with a general type while actual behavior varies by object

  • Two main types: Compile-time polymorphism (method overloading, resolved by compiler) and runtime polymorphism (method overriding, resolved during execution)

  • Method overriding requires inheritance — child class provides specific implementation of parent’s method while maintaining the same signature

  • Python uses duck typing — objects don’t need to inherit from a common base; they just need the right methods (“if it quacks like a duck…”)

  • Polymorphism enables the Open/Closed Principle — add new types without modifying existing code, making systems extensible and maintainable