Polymorphism in OOP: Runtime vs Compile-Time
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.
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_concertfunction works with theAnimalinterface - 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:
Shapeis an abstract base class that defines the interface- You cannot instantiate
Shapedirectly — it forces subclasses to implement required methods print_shape_infoworks with anyShapesubclass 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:
- Create an abstract base class or interface (
PaymentMethod) - Define common method(s) (
process_payment) - Implement concrete classes (
CreditCard,PayPal,Bitcoin) - 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:
- Start with abstract base class
- Define abstract method(s)
- Create 2-3 concrete implementations
- Write a function that uses the base type
- 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