Polymorphism

7 min read

Reading Progress0%
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5

Polymorphism

1. What Is It?

— Greek for "many forms" — is the ability of different objects to respond to the same message (method call) in their own way. A caller writes code against a common or base type, and the correct behavior is automatically selected at runtime based on the actual object's type. The intent, as the GoF authors framed it: clients remain unaware of the specific types of objects they use, as long as those objects adhere to the .

Without , every operation that should work uniformly across types degrades into a chain of if/elif or switch checks on type tags. Adding a new type means touching every such branch — a fragile, open-ended maintenance burden. Polymorphism eliminates those branches by delegating the "which behavior?" question to the object itself.


QUICK CHECK

A payment processing system needs to support multiple payment methods (credit card, PayPal, cryptocurrency). A developer writes the following logic to handle charging: if type == 'credit_card': charge_credit_card() elif type == 'paypal': charge_paypal() elif type == 'crypto': charge_crypto(). A new payment method is added every quarter. What is the primary maintenance problem with this approach, and how does polymorphism address it?

Choose one answer

2. How It Works

Subtype (Runtime) — the core form:

  1. Define a shared or abstract base type with a method contract (e.g., speak()).
  2. Multiple concrete classes each implement speak() in their own way.
  3. A caller holds a reference typed as the /base.
  4. At runtime, the JVM (Java) or Python interpreter dispatches the call to the actual object's method — dynamic dispatch.

Python:

from abc import ABC, abstractmethod
from typing import List


class Animal(ABC):
    @abstractmethod
    def speak(self) -> str:
        ...


class Dog(Animal):
    def speak(self) -> str:
        return "Woof"


class Cat(Animal):
    def speak(self) -> str:
        return "Meow"


class Duck(Animal):
    def speak(self) -> str:
        return "Quack"


def make_noise(animals: List[Animal]) -> None:
    for animal in animals:
        print(animal.speak())  # dynamic dispatch — no isinstance() needed


make_noise([Dog(), Cat(), Duck()])
# Woof
# Meow
# Quack

Java:

import java.util.List;

public abstract class Animal {
    public abstract String speak();
}

public class Dog extends Animal {
    @Override
    public String speak() { return "Woof"; }
}

public class Cat extends Animal {
    @Override
    public String speak() { return "Meow"; }
}

public class Duck extends Animal {
    @Override
    public String speak() { return "Quack"; }
}

public class Main {
    public static void makeNoise(List<Animal> animals) {
        for (Animal animal : animals) {
            System.out.println(animal.speak()); // JVM resolves at runtime
        }
    }

    public static void main(String[] args) {
        makeNoise(List.of(new Dog(), new Cat(), new Duck()));
        // Woof
        // Meow
        // Quack
    }
}

QUICK CHECK

A backend service has a PaymentProcessor abstract base class with an abstract method charge(). Three concrete classes — StripeProcessor, PayPalProcessor, and CryptoProcessor — each extend it with their own charge() implementation. A billing function accepts a List<PaymentProcessor> and calls charge() on each element. When the billing function iterates the list and calls charge(), how does the runtime determine which concrete charge() implementation to execute?

Choose one answer

3. Variants & Comparisons

Forms of Polymorphism

ApproachHow It WorksProsConsBest For
Subtype / OverrideSubclass overrides a base method; runtime dispatch selects the right oneClean, extensible; new types need no caller changesRequires inheritance hierarchyCore domain behavior varying by type
Interface-basedUnrelated classes implement the same interface; caller uses interface typeNo inheritance coupling; maximum flexibilityMore boilerplate (Java)Cross-cutting behaviors (Serializable, Comparable)
Duck Typing (Python)Objects used interchangeably if they have the required method — no shared base neededMinimal boilerplate; extremely flexibleNo compile-time safety; errors surface at runtimeScripting, data pipelines, rapid iteration
Static Overloading (Java)Same method name, different parameter types; compiler picks at compile timeConvenient API designNo runtime flexibility; resolved before executionConvenience constructors, utility methods

Language-Specific Notes

Python:

  • Duck typing is the dominant form: if an object has speak(), you can call speak() on it — no shared base class required.
  • from abc import ABC, abstractmethod enforces contracts explicitly.
  • typing.Protocol (PEP 544, Python 3.8+) enables static duck typing — structural subtyping checked by type checkers, not the runtime.
  • Python does not support traditional overloading (same name, different param counts) — the second definition silently replaces the first. Use default arguments or functools.singledispatch instead.
  • Dunder methods (__len__, __iter__, __str__) make built-in operations polymorphic: len(x) dispatches to x.__len__() for any type that defines it.

Java:

  • Method overriding uses the JVM's dynamic dispatch (late binding). @Override is a compile-time annotation for safety — it does not itself enable .
  • Overloading is resolved at compile time (static binding / early binding) — not runtime .
  • -based polymorphism is idiomatic Java: List<String> list = new ArrayList<>() — code written against List<> works with any implementation.

4. When to Use It (and When NOT To)

Use it when:

  • Multiple types share a behavior that varies in implementation (animals that speak, shapes that draw, payment methods that charge).
  • You want to add new types without modifying existing code (Open/Closed Principle).
  • A function or class should operate on a family of types without knowing their concrete forms.

Anti-patterns to avoid:

  • isinstance() chains as a substitute for . if isinstance(x, Dog): ... elif isinstance(x, Cat): ... is procedural thinking. Every new type requires touching this branch. Move the behavior into the class instead.
  • Overloading as a design substitute. Overloading is a syntactic convenience, not a . Don't use it to fake type-based dispatch.
  • Forcing for polymorphism when fits better. If two classes share an but not an identity, use an /Protocol, not a base class.

Decision triggers:

  • "I have a switch or if/elif on type" → reach for subtype polymorphism.
  • "I want to swap implementations at runtime" → reach for interface-based polymorphism + pattern.
  • "I'm writing Python and don't want to couple to a hierarchy" → reach for duck typing or Protocol.

QUICK CHECK

A backend service processes payments and currently uses this logic: if isinstance(payment, CreditCard): charge_credit(...) elif isinstance(payment, PayPal): charge_paypal(...) elif isinstance(payment, Crypto): charge_crypto(...). A new payment method is added every few months. What is the primary problem with this design, and what is the recommended fix?

Choose one answer

5. Real-World Usage

1. java.util.List ArrayList and LinkedList both implement java.util.List. Application code written against List<String> switches implementations with a one-line change. The Java Collections Framework is built entirely on this model: Collection, Iterable, Comparable, and Comparator are interfaces that make algorithms (sorting, searching) work across arbitrary types.

2. Python's Built-in Protocol len(), for, in, str(), and with all dispatch to dunder methods — making them work on any type that defines the relevant protocol. A custom class defining __len__ and __iter__ integrates with the entire Python ecosystem (e.g., for x in my_object:) without inheriting from anything. Django's ORM, NumPy arrays, and Python's pathlib.Path all exploit this.

3. Java's Comparator / Comparable for Sorting Collections.sort(list, comparator) accepts any Comparator<T> implementation. Different sort orders (by name, by age, by score) are separate classes implementing one . The sort algorithm is completely decoupled from what "ordering" means for any specific type — a direct application of polymorphism enabling the pattern.


QUICK CHECK

A backend service uses Collections.sort(orders, comparator) to sort a list of orders. Currently it sorts by price, but a new requirement needs sorting by delivery date instead. Which approach best demonstrates how polymorphism keeps this change minimal?

Choose one answer

6. Interview Cheat Sheet

Key sentences to say:

  1. " lets me write code against an — the runtime figures out which concrete implementation to invoke, so I never need to branch on type."
  2. "In Java, method overriding is resolved at runtime via dynamic dispatch; overloading is resolved at compile time — only the former is true runtime ."
  3. "Python's duck typing is structural polymorphism — if it has the method, I can call it. typing.Protocol gives you the same thing with static type-checker support."
  4. "The smell that tells me I need polymorphism is an isinstance() chain or a switch on type — that logic belongs inside the objects, not outside them."
  5. "Polymorphism is what makes the Open/Closed Principle practical: I add new types by creating new classes, not by editing existing branches."

Common follow-up questions:

Q: What's the difference between overriding and overloading? Overriding replaces a method in a subclass — resolved at runtime (dynamic dispatch). Overloading provides multiple methods with the same name but different signatures in the same class — resolved at compile time (static dispatch). Only overriding is runtime polymorphism.

Q: Can Python do method overloading? No in the traditional sense — the second definition overwrites the first. Use default arguments, *args/**kwargs, or functools.singledispatch for type-based dispatch. @typing.overload exists only for type-checker hints, not runtime behavior.

Q: What's the difference between duck typing and structural typing? Duck typing is runtime: Python calls the method when you actually invoke it and raises AttributeError if it's missing. Structural typing (via typing.Protocol) is the static-analysis equivalent — mypy/pyright verify structural compatibility at check time without the program running.

Q: When would you use an vs. an for polymorphism? when the types share both and partial implementation (common or behavior). Interface/Protocol when you only want to enforce a contract with no shared implementation — especially when the types are otherwise unrelated. Prefer interfaces; they impose less .

Connections to other concepts:

  • Open/Closed Principle (OCP): Polymorphism is the mechanism that makes OCP achievable — new subtypes extend behavior without modifying existing code.
  • Pattern: Directly exploits polymorphism: a family of interchangeable algorithms behind a common interface, swappable at runtime.
  • Principle (LSP): Defines the correctness constraint on polymorphism — a subtype must be substitutable for its base type without breaking the program.
  • Principle (DIP): High-level modules depend on abstractions (interfaces), and polymorphism is how those abstractions get fulfilled by concrete implementations.
Glossary History

Click dotted jargon to save explanations here.

Glossary History

Click dotted jargon to save explanations here.