Polymorphism in object-oriented programming is the ability of one interface or method call to work with objects of different types, with each object responding in a way that suits its own class. A caller can ask any shape to draw itself or any payment method to process a charge, without knowing which concrete class it is dealing with.
The word comes from Greek for many forms. Polymorphism is one of the core principles of object-oriented design, alongside encapsulation and inheritance, and it is the main reason object-oriented code can be extended with new types without changing existing callers.
How polymorphism works at run time
Run-time polymorphism works through dynamic dispatch: the program decides which method implementation to execute based on the actual class of the object, not on the declared type of the variable that refers to it. A variable of type Shape may hold a Circle or a Rectangle, and calling draw runs the version defined by that specific object.
Most compiled object-oriented languages implement this with a virtual method table (vtable). Each class has a table of pointers to its method implementations, and each object holds a hidden pointer to its class table. A polymorphic call loads that pointer, looks up the method slot and jumps to it, which adds one or two memory indirections compared with a direct call.
In C++, a method takes part in dynamic dispatch only when declared virtual. Instance methods are virtual by default in Java but opt-in through the virtual keyword in C#, while dynamically typed languages such as Python resolve every method call at run time by looking up the name on the object.
Kinds of polymorphism
Polymorphism is an umbrella term for several distinct mechanisms. Computer science usually distinguishes subtype, parametric and ad hoc polymorphism, and the distinction matters because each is resolved at a different time and suits different problems.
| Kind | Mechanism | Resolved | Example |
|---|---|---|---|
| Subtype polymorphism | Overriding methods of a base class or interface | At run time | Shape.draw on Circle or Square |
| Parametric polymorphism | Generic code over any type | Usually at compile time | List of T, generic functions |
| Ad hoc polymorphism | Overloading by parameter types | At compile time | print for integers and strings |
| Duck typing | Any object with the right methods is accepted | At run time | Python objects with a read method |
| Operator overloading | Operators defined per type | At compile time or run time | Adding two vectors with + |
Languages without classical inheritance still offer polymorphism. Go uses implicitly satisfied interfaces, Rust uses traits with either static or dynamic dispatch, and Haskell uses type classes.
Polymorphism compared with inheritance and interfaces
Polymorphism is often confused with inheritance, yet inheritance is only one way to achieve it. Inheritance reuses and extends code from a parent class, while polymorphism concerns treating different types uniformly through a shared contract.
| Concept | Main purpose | Relation to polymorphism |
|---|---|---|
| Inheritance | Reuse and extend a base class | Provides subtype polymorphism through overriding |
| Interface | Define a contract without implementation | Cleanest basis for polymorphic calls |
| Encapsulation | Hide internal state behind methods | Lets each type implement behavior privately |
| Abstract class | Partial implementation with required methods | Mix of shared code and polymorphic hooks |
| Composition | Build objects from other objects | Often combined with interfaces instead of inheritance |
Modern design guidance often recommends programming to an interface rather than an implementation and preferring composition over deep inheritance hierarchies, because interfaces give polymorphism without the tight coupling of a shared base class.
Using polymorphism in design
Polymorphism is most useful where code would otherwise branch on the type of an object. A long chain of if or switch statements that checks a type field is a classic signal that a polymorphic method should replace it.
| Situation | Polymorphic approach | Benefit |
|---|---|---|
| Many types share an action but differ in details | Common interface with overridden methods | Callers stay unchanged |
| New variants are added frequently | New class implements the interface | Follows the open-closed principle |
| Algorithm varies at run time | Strategy pattern | Behavior is swapped without conditionals |
| Tests need a replacement for a slow dependency | Mock or fake implementing the same interface | Isolated, fast unit tests |
| Set of types is fixed and small | Pattern matching on a closed type may suit better | Exhaustiveness checked by compiler |
Polymorphism also supports the Liskov substitution principle: any object of a subtype must be usable wherever its base type is expected without breaking the caller’s assumptions.
Pitfalls and misconceptions
Polymorphism brings flexibility at the cost of indirection and less obvious control flow. Experienced developers watch for several recurring problems.
- Violating substitutability: a subclass that throws on an inherited method or changes its meaning breaks every polymorphic caller.
- Overloading mistaken for overriding: an overload is chosen at compile time from declared types, so it does not dispatch dynamically.
- Missing virtual destructor in C++: deleting a derived object through a base pointer without a virtual destructor causes undefined behavior.
- Object slicing: copying a derived object into a base-class value in C++ discards the derived part and its overrides.
- Over-abstraction: interfaces with a single implementation and no expected variation add indirection without benefit.
- Performance in hot loops: dynamic dispatch can prevent inlining, although the cost is negligible in most application code.
Explained without jargon
Polymorphism means one request, many correct responses. You give the same instruction to different things, and each one carries it out in its own way.
Think of saying “play” to a music player, a video player and a game console. Each one starts something different, yet you used the same word. You do not need to know how each device works inside. In software, this lets programmers add new kinds of objects without rewriting the code that uses them.
A typical use case
An online store’s checkout supports card payments and bank transfers. The original code contains a large switch statement that checks the payment type in five different places, and adding a digital wallet would require editing all of them.
The team defines a PaymentMethod interface with authorize, capture and refund methods. Each payment type becomes a class that implements the interface, and the checkout calls the methods without checking which class it holds. Adding the digital wallet now means writing one new class and registering it.
Unit tests use a fake payment class that implements the same interface, so checkout logic is tested in milliseconds without contacting a real payment provider. The result is fewer touched files per change and a measurable drop in payment-related defects after each release.