1. What Is It?
— Greek for "many forms" — is the ability of different objects to respond to the same message (method call) in their own way. A caller writes code against a common or base type, and the correct behavior is automatically selected at runtime based on the actual object's type. The intent, as the GoF authors framed it: clients remain unaware of the specific types of objects they use, as long as those objects adhere to the .
Without , every operation that should work uniformly across types degrades into a chain of if/elif or switch checks on type tags. Adding a new type means touching every such branch — a fragile, open-ended maintenance burden. Polymorphism eliminates those branches by delegating the "which behavior?" question to the object itself.
A payment processing system needs to support multiple payment methods (credit card, PayPal, cryptocurrency). A developer writes the following logic to handle charging: if type == 'credit_card': charge_credit_card() elif type == 'paypal': charge_paypal() elif type == 'crypto': charge_crypto(). A new payment method is added every quarter. What is the primary maintenance problem with this approach, and how does polymorphism address it?
2. How It Works
Subtype (Runtime) — the core form:
- Define a shared or abstract base type with a method contract (e.g.,
speak()). - Multiple concrete classes each implement
speak()in their own way. - A caller holds a reference typed as the /base.
- At runtime, the JVM (Java) or Python interpreter dispatches the call to the actual object's method — dynamic dispatch.
Python:
from abc import ABC, abstractmethod
from typing import List
class Animal(ABC):
@abstractmethod
def speak(self) -> str:
...
class Dog(Animal):
def speak(self) -> str:
return "Woof"
class Cat(Animal):
def speak(self) -> str:
return "Meow"
class Duck(Animal):
def speak(self) -> str:
return "Quack"
def make_noise(animals: List[Animal]) -> None:
for animal in animals:
print(animal.speak()) # dynamic dispatch — no isinstance() needed
make_noise([Dog(), Cat(), Duck()])
# Woof
# Meow
# QuackJava:
import java.util.List;
public abstract class Animal {
public abstract String speak();
}
public class Dog extends Animal {
@Override
public String speak() { return "Woof"; }
}
public class Cat extends Animal {
@Override
public String speak() { return "Meow"; }
}
public class Duck extends Animal {
@Override
public String speak() { return "Quack"; }
}
public class Main {
public static void makeNoise(List<Animal> animals) {
for (Animal animal : animals) {
System.out.println(animal.speak()); // JVM resolves at runtime
}
}
public static void main(String[] args) {
makeNoise(List.of(new Dog(), new Cat(), new Duck()));
// Woof
// Meow
// Quack
}
}One call selects each animal’s behavior
The same speak call selects Dog, Cat, or Duck behavior from the actual object at runtime.
The caller accepts animals through a shared contract, so it does not branch on their concrete types.
Text version
- The caller accepts animals through a shared contract, so it does not branch on their concrete types.
- Dog returns Woof, which the caller prints through the shared speak call.
- Cat returns Meow, so the same print statement produces a different sound.
- Duck returns Quack, which the same caller prints without a type branch.
A backend service has a PaymentProcessor abstract base class with an abstract method charge(). Three concrete classes — StripeProcessor, PayPalProcessor, and CryptoProcessor — each extend it with their own charge() implementation. A billing function accepts a List<PaymentProcessor> and calls charge() on each element. When the billing function iterates the list and calls charge(), how does the runtime determine which concrete charge() implementation to execute?
3. Variants & Comparisons
Forms of Polymorphism
| Approach | How It Works | Pros | Cons | Best For |
|---|---|---|---|---|
| Subtype / Override | Subclass overrides a base method; runtime dispatch selects the right one | Clean, extensible; new types need no caller changes | Requires inheritance hierarchy | Core domain behavior varying by type |
| Interface-based | Unrelated classes implement the same interface; caller uses interface type | No inheritance coupling; maximum flexibility | More boilerplate (Java) | Cross-cutting behaviors (Serializable, Comparable) |
| Duck Typing (Python) | Objects used interchangeably if they have the required method — no shared base needed | Minimal boilerplate; extremely flexible | No compile-time safety; errors surface at runtime | Scripting, data pipelines, rapid iteration |
| Static Overloading (Java) | Same method name, different parameter types; compiler picks at compile time | Convenient API design | No runtime flexibility; resolved before execution | Convenience constructors, utility methods |
Language-Specific Notes
Python:
- Duck typing is the dominant form: if an object has
speak(), you can callspeak()on it — no shared base class required. from abc import ABC, abstractmethodenforces contracts explicitly.typing.Protocol(PEP 544, Python 3.8+) enables static duck typing — structural subtyping checked by type checkers, not the runtime.- Python does not support traditional overloading (same name, different param counts) — the second definition silently replaces the first. Use default arguments or
functools.singledispatchinstead. - Dunder methods (
__len__,__iter__,__str__) make built-in operations polymorphic:len(x)dispatches tox.__len__()for any type that defines it.
Java:
- Method overriding uses the JVM's dynamic dispatch (late binding).
@Overrideis a compile-time annotation for safety — it does not itself enable . - Overloading is resolved at compile time (static binding / early binding) — not runtime .
- -based polymorphism is idiomatic Java:
List<String> list = new ArrayList<>()— code written againstList<>works with any implementation.
Overriding and overloading bind at different times
Java overriding selects behavior at runtime; overloading selects a signature at compile time.
Overriding keeps the signature and replaces the inherited method body, but overloading keeps the name and changes parameter types or counts.
Text version
- Overriding keeps the signature and replaces the inherited method body, but overloading keeps the name and changes parameter types or counts.
- Java selects an override at runtime, but the compiler resolves an overload before execution.
- Use overriding for behavior that varies by type, and overloading for convenient APIs.
4. When to Use It (and When NOT To)
Use it when:
- Multiple types share a behavior that varies in implementation (animals that speak, shapes that draw, payment methods that charge).
- You want to add new types without modifying existing code (Open/Closed Principle).
- A function or class should operate on a family of types without knowing their concrete forms.
Anti-patterns to avoid:
isinstance()chains as a substitute for .if isinstance(x, Dog): ... elif isinstance(x, Cat): ...is procedural thinking. Every new type requires touching this branch. Move the behavior into the class instead.- Overloading as a design substitute. Overloading is a syntactic convenience, not a . Don't use it to fake type-based dispatch.
- Forcing for polymorphism when fits better. If two classes share an but not an identity, use an /Protocol, not a base class.
Decision triggers:
- "I have a
switchorif/elifon type" → reach for subtype polymorphism. - "I want to swap implementations at runtime" → reach for interface-based polymorphism + pattern.
- "I'm writing Python and don't want to couple to a hierarchy" → reach for duck typing or
Protocol.
A backend service processes payments and currently uses this logic: if isinstance(payment, CreditCard): charge_credit(...) elif isinstance(payment, PayPal): charge_paypal(...) elif isinstance(payment, Crypto): charge_crypto(...). A new payment method is added every few months. What is the primary problem with this design, and what is the recommended fix?
5. Real-World Usage
1. java.util.List —
ArrayList and LinkedList both implement java.util.List. Application code written against List<String> switches implementations with a one-line change. The Java Collections Framework is built entirely on this model: Collection, Iterable, Comparable, and Comparator are interfaces that make algorithms (sorting, searching) work across arbitrary types.
2. Python's Built-in Protocol
len(), for, in, str(), and with all dispatch to dunder methods — making them work on any type that defines the relevant protocol. A custom class defining __len__ and __iter__ integrates with the entire Python ecosystem (e.g., for x in my_object:) without inheriting from anything. Django's ORM, NumPy arrays, and Python's pathlib.Path all exploit this.
3. Java's Comparator / Comparable for Sorting
Collections.sort(list, comparator) accepts any Comparator<T> implementation. Different sort orders (by name, by age, by score) are separate classes implementing one . The sort algorithm is completely decoupled from what "ordering" means for any specific type — a direct application of polymorphism enabling the pattern.
A backend service uses Collections.sort(orders, comparator) to sort a list of orders. Currently it sorts by price, but a new requirement needs sorting by delivery date instead. Which approach best demonstrates how polymorphism keeps this change minimal?
6. Interview Cheat Sheet
Key sentences to say:
- " lets me write code against an — the runtime figures out which concrete implementation to invoke, so I never need to branch on type."
- "In Java, method overriding is resolved at runtime via dynamic dispatch; overloading is resolved at compile time — only the former is true runtime ."
- "Python's duck typing is structural polymorphism — if it has the method, I can call it.
typing.Protocolgives you the same thing with static type-checker support." - "The smell that tells me I need polymorphism is an
isinstance()chain or aswitchon type — that logic belongs inside the objects, not outside them." - "Polymorphism is what makes the Open/Closed Principle practical: I add new types by creating new classes, not by editing existing branches."
Common follow-up questions:
Q: What's the difference between overriding and overloading? Overriding replaces a method in a subclass — resolved at runtime (dynamic dispatch). Overloading provides multiple methods with the same name but different signatures in the same class — resolved at compile time (static dispatch). Only overriding is runtime polymorphism.
Q: Can Python do method overloading?
No in the traditional sense — the second definition overwrites the first. Use default arguments, *args/**kwargs, or functools.singledispatch for type-based dispatch. @typing.overload exists only for type-checker hints, not runtime behavior.
Q: What's the difference between duck typing and structural typing?
Duck typing is runtime: Python calls the method when you actually invoke it and raises AttributeError if it's missing. Structural typing (via typing.Protocol) is the static-analysis equivalent — mypy/pyright verify structural compatibility at check time without the program running.
Q: When would you use an vs. an for polymorphism? when the types share both and partial implementation (common or behavior). Interface/Protocol when you only want to enforce a contract with no shared implementation — especially when the types are otherwise unrelated. Prefer interfaces; they impose less .
Connections to other concepts:
- Open/Closed Principle (OCP): Polymorphism is the mechanism that makes OCP achievable — new subtypes extend behavior without modifying existing code.
- Pattern: Directly exploits polymorphism: a family of interchangeable algorithms behind a common interface, swappable at runtime.
- Principle (LSP): Defines the correctness constraint on polymorphism — a subtype must be substitutable for its base type without breaking the program.
- Principle (DIP): High-level modules depend on abstractions (interfaces), and polymorphism is how those abstractions get fulfilled by concrete implementations.