Polymorphism in C++, Java and Python: Code Examples Explained

Polymorphism lets a single method call take many forms. This practical guide explains compile-time vs runtime polymorphism and walks through runnable C++, Java, and Python examples you can try right away.

Diagram showing a Shape interface with an area() method dispatched to Circle, Square, and Triangle, each computing area differently, illustrating polymorphism in OOP.

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?

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.

Side-by-side comparison showing compile-time polymorphism, where the compiler picks an overloaded add() method by argument type, versus runtime polymorphism, where the object's real type decides which area() implementation runs.
AspectCompile-time (static)Runtime (dynamic)
Resolved byThe compilerThe running program
MechanismMethod / operator overloadingMethod overriding, virtual functions
Chosen usingArgument types and countThe object’s actual type
SpeedSlightly 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
Diagram of duck typing in Python where one function calls speak() on three unrelated classes, Dog, Cat, and Duck, each returning a different sound, showing polymorphism without inheritance.

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 Triangle class 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/else or switch chains 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 virtual in 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++, use override.
  • 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.

Frequently asked questions

Continue learning OOP

Scroll to Top