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 tested examples in C++, Java and Python.
Every program on this page was run on Ubuntu 24.04 with GCC 13.3 and Clang 18.1.3 (-std=c++17 -Wall -Wextra -pedantic -Werror), OpenJDK 21 (javac -Xlint:all -Werror) and Python 3.11, 3.12 and 3.13 (-W error), and every output below is captured verbatim. The code is in a GitHub repository whose build repeats these checks on each commit.
The Short Answer
What is polymorphism in OOP? The ability of one call, written once, to run different code depending on the types involved. shape.area() runs the circle’s formula for a circle and the rectangle’s for a rectangle, and the calling code doesn’t change.
What is the difference between compile-time and runtime polymorphism? Compile-time polymorphism is resolved by the compiler, from argument types (overloading) or type parameters (templates and generics). Runtime polymorphism is resolved while the program runs, from the object’s actual type (overriding and virtual functions).
Is polymorphism the same as inheritance? No. Inheritance is how classes share structure and behavior; polymorphism is how a shared call resolves to the right implementation. Runtime polymorphism usually relies on inheritance, but Python’s duck typing shows it can work without it.
What is polymorphism?
In object-oriented programming, polymorphism is the ability of one interface, such as a method name or a function call, to work with values of different types, running the behavior that fits each type. In its most common form, runtime polymorphism, the choice depends 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 runtime polymorphism in a class hierarchy, a few conditions apply in C++ and Java (Python needs only the method, as the duck typing example shows):
- 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 |
| Cost | None at run time; the call can be inlined | An indirect call through the object’s dispatch table, which the compiler or JIT often removes when it can prove the type; the larger cost is usually lost inlining |
The code sections below show both kinds in each language. Overloading demonstrates compile-time polymorphism; overriding demonstrates runtime polymorphism.
Compile-time polymorphism has a second form besides overloading. Parametric polymorphism is code written once with a type parameter, which then works for many types: templates in C++ and generics in Java. Computer science usually names three kinds of polymorphism, following Luca Cardelli and Peter Wegner (1985):
| Kind | Chosen | C++ | Java | Python |
|---|---|---|---|---|
| Ad hoc (overloading) | Compile time | Several add() functions with different parameter types | Several add() methods with different parameter types | Not supported; functools.singledispatch chooses at run time by the first argument’s type |
| Parametric (generics) | Compile time | template <typename T> T first(const std::vector<T>& items) | static <T> T first(List<T> items) | def first[T](items: list[T]) -> T (3.12+), checked only by type checkers |
| Subtype (overriding) | Run time | virtual functions, marked override | Every instance method that is not private, static or final | Every method |
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.
// shapes.cpp - runtime polymorphism: the loop calls area() and name()
// through a Shape pointer, and each object runs its own version.
#include <iomanip>
#include <iostream>
#include <memory>
#include <string>
#include <vector>
constexpr double kPi = 3.141592653589793;
class Shape {
public:
virtual double area() const = 0; // pure virtual: must be overridden
virtual std::string name() const = 0;
virtual ~Shape() = default; // a polymorphic base needs a virtual destructor
};
class Circle : public Shape {
double radius;
public:
explicit Circle(double r) : radius(r) {}
double area() const override { return kPi * 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));
std::cout << std::fixed << std::setprecision(2);
for (const auto& shape : shapes)
std::cout << shape->name() << " area: " << shape->area() << '\n';
}
Output:
Circle area: 12.57
Rectangle area: 12.00
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; in practice the derived destructor usually does not run, so whatever it would have released is leaked.
Compile-time polymorphism with overloading
// overloading.cpp - compile-time polymorphism: the compiler picks an add()
// from the argument types before the program runs.
#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';
std::cout << add(2.5, 3.5) << '\n';
std::cout << add(std::string("poly"), std::string("morphism")) << '\n';
}
Output:
5
6
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 instance methods are virtual by default (all except private, static and final ones), 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.
// Shapes.java - runtime polymorphism: the loop calls area() and name()
// through a Shape reference, and each object runs its own version.
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 Shapes {
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());
}
}
Output:
Circle area: 12.57
Rectangle area: 12.00
Compile-time polymorphism with overloading
// Overloading.java - compile-time polymorphism: the compiler picks an add()
// from the argument types before the program runs.
public class Overloading {
static int add(int a, int b) { return a + b; }
static double add(double a, double b) { return a + b; }
static String add(String a, String b) { return a + b; }
public static void main(String[] args) {
System.out.println(add(2, 3));
System.out.println(add(2.5, 3.5));
System.out.println(add("poly", "morphism"));
}
}
Output:
5
6.0
polymorphism
Java prints 6.0 where C++ printed 6, because Java converts a double to text with at least one digit after the decimal point.
Polymorphism in Python
Runtime polymorphism with overriding
Python supports overriding the same way, often using the abc module to define an abstract base class.
"""shapes.py - runtime polymorphism: the loop calls area() and name()
on each shape, and each object runs its own version."""
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}")
Output:
Circle area: 12.57
Rectangle area: 12.00
The output is identical to the C++ and Java versions.
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.
"""duck_typing.py - polymorphism without inheritance: any object with a
speak() method works, whatever its class."""
class Dog:
def speak(self) -> str:
return "Woof"
class Cat:
def speak(self) -> str:
return "Meow"
class Duck:
def speak(self) -> str:
return "Quack"
def make_it_speak(animal) -> None:
# No common base class required: only that speak() exists.
print(animal.speak())
for animal in (Dog(), Cat(), Duck()):
make_it_speak(animal)
Output:
Woof
Meow
Quack

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, a call through a base pointer or reference runs the base version, and the derived function only hides it. Marking the derived functionoverrideturns the mistake into a compile error (GCC:marked 'override', but does not override). - 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. The C++ object-oriented programming guide shows slicing with measured output.
Key Takeaways
- Polymorphism lets one call run different code for different types. The same loop printed a circle’s and a rectangle’s area in all three languages.
- Compile-time polymorphism is chosen by the compiler: overloading picks by argument types, and templates and generics by type parameters.
- Runtime polymorphism is chosen by the object: virtual functions in C++, instance methods in Java, every method in Python.
- Python needs no shared base class. Any object with the right method works, which is duck typing.
- In C++, mark overrides
override, give polymorphic bases a virtual destructor, and use pointers or references, never copies, to avoid slicing.
Conclusion
Polymorphism is what lets code be written against an interface instead of a list of types: add a Triangle and every loop over shapes keeps working. C++, Java and Python reach it differently. C++ asks for virtual and gives a choice between compile-time and run-time dispatch; Java makes instance methods virtual by default; Python resolves every call at run time and doesn’t require a shared base class.
The other concept pages in this series cover the pieces polymorphism builds on: classes, objects, encapsulation, abstraction and inheritance. For the C++ mechanics of virtual dispatch, see pure virtual functions in C++. More topics are collected in the OOP section.
Source Code and Tests
All programs on this page are in the oop/polymorphism-concepts directory of the MYCPLUS C++ examples repository.
Build and test:
bash tests/run_tests.sh g++
What the build checks. On Linux, with GCC and with Clang, it compiles the C++ programs with -Wall -Wextra -pedantic -Werror, compiles the Java programs with javac -Xlint:all -Werror on Java 21, runs the Python programs with -W error on Python 3.12, and checks that every program prints exactly the output shown on this page. The shapes example prints the same in all three languages; the overloading example has one expected output per language. Python 3.11 and 3.13 were checked locally and produced the same output. The generics declarations in the comparison table and the override compile error were checked by hand and are not part of the automated build.




