7 min read
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
Tier 2
Tier 3
Tier 4
Tier 5
Polymorphism
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 # Quack
Java:
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 } }
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.
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.
Glossary History
Click dotted jargon to save explanations here.
Glossary History
Click dotted jargon to save explanations here.