Polymorphism is one of those words that sounds harder than the idea. Strip away the Greek and it means "many forms": a single interface that can stand in for many underlying types, each doing the right thing when called. Write code against the shared shape, and the specific behavior gets chosen for you.
What changed in 2026
- Interfaces beat inheritance in the mainstream. Modern style guides across Go, Rust, TypeScript, and even newer Java code favor small interfaces and composition over deep class hierarchies.
- Generics landed everywhere. With Go generics mature and TypeScript generics ubiquitous, parametric polymorphism is now a tool most working developers reach for weekly, not an academic curiosity.
- Pattern matching blurred the lines. Exhaustive pattern matching in more languages gives an alternative to subtype dispatch, so "add a method to each type" is no longer the only clean answer.
The three kinds of polymorphism
Most confusion comes from treating polymorphism as one thing. It is at least three.
- Subtype (runtime) polymorphism. A
Shape reference might point to a Circle or a Square; calling area() dispatches to the right implementation at runtime. This is the classic OOP form.
- Parametric polymorphism. Generics. A
List<T> works for any T without knowing the type in advance. One implementation, many concrete types, checked at compile time.
- Ad hoc polymorphism. Overloading and operator overloading. The same name (
+, or a function print) behaves differently based on argument types.
Types of polymorphism compared
| Kind |
Selected at |
Example |
Buys you |
| Subtype |
Runtime |
shape.area() on Circle vs Square |
Extensible behavior behind a shared interface |
| Parametric |
Compile time |
List<T>, Map<K, V> |
Reuse across types without losing type safety |
| Ad hoc |
Compile time |
add(int, int) vs add(str, str) |
Convenient shared names for related operations |
They are not competitors — real codebases use all three, often in the same file.
How dynamic dispatch works
Subtype polymorphism usually relies on dynamic dispatch. Under the hood, each object carries a reference to a table of function pointers (a vtable in C++, an interface table elsewhere). When you call area(), the runtime looks up the concrete implementation for that object's actual type and jumps to it.
The cost is one extra indirection and a missed inlining opportunity — usually negligible, occasionally meaningful in hot loops. This is why performance-critical code sometimes prefers generics (resolved at compile time) over virtual calls. If you care about that boundary, the tradeoffs mirror the ones in compiled vs interpreted languages.
When polymorphism earns its keep
Polymorphism pays off when you find yourself writing conditionals that branch on a type tag:
// smell: switching on type
switch (shape.kind) {
case "circle": return Math.PI * r * r;
case "square": return side * side;
}
Every new shape means editing that switch — in every place it appears. Give each type an area() method behind a Shape interface and adding a triangle touches exactly one new file. That is the real value: new behavior by adding code, not editing existing code.
But it is not free. Indirection makes control flow harder to follow. If you have exactly two cases that will never grow, a plain conditional is clearer than a class hierarchy.
Common pitfalls
Inheritance for code reuse. The biggest trap. Extending a class just to grab its methods creates a fragile is-a relationship that is not real. Prefer composition or a shared interface.
Leaky abstractions. If callers have to type-check or downcast to make the interface work, the polymorphism is fake and the abstraction is wrong.
Over-generic code. Parametric polymorphism can be taken too far — a function with five type parameters and three constraints is often harder to read than three concrete functions.
Deep hierarchies. Every level of inheritance multiplies the mental model needed to understand behavior. Keep hierarchies shallow.
FAQ
Is polymorphism the same as inheritance?
No. Inheritance is one mechanism that enables subtype polymorphism, but interfaces provide it without inheritance, and generics provide a different kind entirely. You can have polymorphism with zero inheritance.
What is the difference between overloading and overriding?
Overloading is ad hoc polymorphism resolved at compile time by argument types. Overriding is subtype polymorphism resolved at runtime by the object's actual type. Different mechanisms, different timing.
Do dynamic languages have polymorphism?
Yes, via duck typing — if an object responds to the method you call, it works, regardless of declared type. It is subtype polymorphism without the static contract.
Is polymorphism slow?
Runtime dispatch adds a small indirection cost that rarely matters. When it does, in tight loops, generics or monomorphization avoid it by resolving types at compile time.
Where to go next