Polymorphism is one of the four pillars of object-oriented programming, alongside encapsulation, inheritance, and abstraction. In plain terms, polymorphism means “many forms”: a single method name, interface, or variable can behave differently depending on the type of object it is working with. This guide explains how polymorphism works, the difference between compile-time and runtime polymorphism, and shows complete, runnable examples in C++, Java, and Python that you can paste straight into a compiler or interpreter.
Table of Contents
- What is polymorphism?
- The two types: compile-time vs runtime polymorphism
- Polymorphism in C++
- Polymorphism in Java
- Polymorphism in Python
- Why polymorphism matters
- Common pitfalls
- Frequently asked questions
- Continue learning OOP
What is polymorphism?
In object-oriented programming, polymorphism is the ability to resolve a method call to the correct behavior based on the actual type of the object, rather than the declared type of the variable holding it. The same instruction—say, shape.area()—can run completely different code depending on whether shape is a circle, a rectangle, or a triangle.
A helpful real-world analogy: the word “draw” means one thing to an artist, another to a cowboy, and another to a card player. The instruction is identical, but the behavior depends on who receives it. Polymorphism brings that same flexibility to code, so a single call site can drive many different implementations behind a shared interface.
For polymorphism to work in a class hierarchy, a few conditions generally apply:
- The method exists in a common base class or interface.
- Each derived type provides its own implementation of that method.
- The method signatures match between base and derived types (when overriding).
- Objects are referenced through the base type, so the runtime can choose the right implementation.
Key idea: Polymorphism applies to behavior (methods), not to data. You override what an object does, not the raw fields it stores.
The two types: compile-time vs runtime polymorphism
Polymorphism comes in two flavors, separated by when the correct method is chosen.

| Aspect | Compile-time (static) | Runtime (dynamic) |
|---|---|---|
| Resolved by | The compiler | The running program |
| Mechanism | Method / operator overloading | Method overriding, virtual functions |
| Chosen using | Argument types and count | The object’s actual type |
| Speed | Slightly faster (no lookup) | Tiny dispatch overhead |
The code sections below show both kinds in each language. Overloading demonstrates compile-time polymorphism; overriding demonstrates runtime polymorphism.
Polymorphism in C++
Runtime polymorphism with virtual functions
In C++, runtime polymorphism is achieved with virtual functions. When you call a virtual method through a base-class pointer or reference, C++ dispatches to the derived class’s override at run time.
#include <iostream>
#include <memory>
#include <vector>
class Shape {
public:
virtual double area() const = 0; // pure virtual: must be overridden
virtual std::string name() const = 0;
virtual ~Shape() = default; // always make base destructors virtual
};
class Circle : public Shape {
double radius;
public:
explicit Circle(double r) : radius(r) {}
double area() const override { return 3.14159265 * radius * radius; }
std::string name() const override { return "Circle"; }
};
class Rectangle : public Shape {
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override { return width * height; }
std::string name() const override { return "Rectangle"; }
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(2.0));
shapes.push_back(std::make_unique<Rectangle>(3.0, 4.0));
for (const auto& shape : shapes)
std::cout << shape->name() << " area: " << shape->area() << "\n";
// Circle area: 12.5664
// Rectangle area: 12
}
The loop never asks what kind of shape it holds. Each object knows its own area(), and the right version runs automatically—that is runtime polymorphism.
Watch out: Always declare a virtual destructor in a polymorphic base class. Deleting a derived object through a base pointer without one is undefined behavior and leaks resources.
Compile-time polymorphism with overloading
#include <iostream>
#include <string>
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
std::string add(const std::string& a,
const std::string& b) { return a + b; }
int main() {
std::cout << add(2, 3) << "\n"; // 5
std::cout << add(2.5, 3.5) << "\n"; // 6
std::cout << add(std::string("poly"),
std::string("morphism")) << "\n"; // polymorphism
}
The compiler picks which add to call from the argument types—decided before the program runs.
Polymorphism in Java
Runtime polymorphism with method overriding
Java methods are virtual by default, so overriding a method in a subclass gives you runtime polymorphism automatically. The @Override annotation is optional but strongly recommended—it lets the compiler catch signature mistakes.
abstract class Shape {
abstract double area();
abstract String name();
}
class Circle extends Shape {
private final double radius;
Circle(double radius) { this.radius = radius; }
@Override double area() { return Math.PI * radius * radius; }
@Override String name() { return "Circle"; }
}
class Rectangle extends Shape {
private final double width, height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override double area() { return width * height; }
@Override String name() { return "Rectangle"; }
}
public class Main {
public static void main(String[] args) {
Shape[] shapes = { new Circle(2.0), new Rectangle(3.0, 4.0) };
for (Shape shape : shapes)
System.out.printf("%s area: %.2f%n", shape.name(), shape.area());
// Circle area: 12.57
// Rectangle area: 12.00
}
}
Compile-time polymorphism with overloading
class Calculator {
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
String add(String a, String b) { return a + b; }
}
// add(2, 3) -> 5 (int version)
// add(2.5, 3.5) -> 6.0 (double version)
// add("poly", "morphism") -> "polymorphism" (String version)
Polymorphism in Python
Runtime polymorphism with overriding
Python supports overriding the same way, often using the abc module to define an abstract base class.
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def name(self) -> str: ...
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return math.pi * self.radius ** 2
def name(self) -> str:
return "Circle"
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def name(self) -> str:
return "Rectangle"
shapes = [Circle(2.0), Rectangle(3.0, 4.0)]
for shape in shapes:
print(f"{shape.name()} area: {shape.area():.2f}")
# Circle area: 12.57
# Rectangle area: 12.00
Duck typing: polymorphism without inheritance
Python takes polymorphism a step further with duck typing: “if it walks like a duck and quacks like a duck, treat it like a duck.” You don’t need a shared base class—any object exposing the expected method works.
class Dog:
def speak(self) -> str:
return "Woof"
class Cat:
def speak(self) -> str:
return "Meow"
def make_it_speak(animal) -> None:
# No common base class required — only that speak() exists.
print(animal.speak())
for animal in (Dog(), Cat()):
make_it_speak(animal)
# Woof
# Meow

Note: Because Python resolves attributes at run time, it has no method overloading in the C++/Java sense. The same effect is achieved with default arguments, *args, or functools.singledispatch.
Why polymorphism matters
Polymorphism is not just an academic concept—it directly improves real codebases:
- Extensibility. Add a new
Triangleclass and every existing loop over shapes works unchanged. You extend behavior without editing—or even re-reading—the calling code. - Less branching. Polymorphism replaces long
if/elseorswitchchains on type with a single, clean method call. - Loose coupling. Code depends on a shared interface, not on concrete classes, which makes systems easier to test and maintain.
- Reusability. Generic algorithms (sorting, rendering, serializing) can operate on any type that satisfies the interface.
This pairs naturally with inheritance and the broader set of object-oriented programming techniques.
Common pitfalls
- Forgetting
virtualin C++. Without it, calls bind to the static type and overriding silently does nothing. - Mismatched signatures. A method that doesn’t exactly match the base signature creates a new method instead of overriding. In Java, always use
@Override; in C++, useoverride. - Confusing overloading with overriding. Overloading is same name, different parameters (compile-time). Overriding is same signature, different class (runtime).
- Object slicing in C++. Storing derived objects by value in a base-type container strips the derived part. Use pointers or smart pointers instead.


