OOP

Polymorphism

Polymorphism lets one interface represent many underlying forms, so the same call can invoke different behavior depending on the actual object's type.

Many forms.

Polymorphism (Greek for "many forms") means code written against a general type works with any specific subtype, each behaving in its own way. Call shape.area() on a Circle or a Square — the same method name runs different code.

bad.py
# Problem: the caller must know every concrete type and branch on it.
def total_area(shapes):
    total = 0
    for s in shapes:
        if s.kind == "circle":
            total += 3.14 * s.r * s.r
        elif s.kind == "square":
            total += s.side * s.side
        # every new shape forces an edit here
    return total
good.py
# Fix: each type implements the same method; the loop stays stable.
class Circle:
    def __init__(self, r): self.r = r
    def area(self): return 3.14 * self.r * self.r

class Square:
    def __init__(self, side): self.side = side
    def area(self): return self.side * self.side

def total_area(shapes):
    # Polymorphism: same call, different implementations.
    return sum(s.area() for s in shapes)

print(total_area([Circle(2), Square(3)]))  # no isinstance checks

You don't need if isinstance(...) checks — the object knows how to compute its own area.

Overriding vs overloading.

Overriding (runtime)Subclass redefines a method; the actual type decides at runtime (dynamic dispatch).
Overloading (compile-time)Same method name, different parameters; resolved by the compiler.
Parametric (generics)One implementation works over many types (List<T>).
Duck typing"If it quacks like a duck" — behavior by available methods, not declared type.

Dynamic dispatch.

  • Dynamic dispatch — virtual calls resolved via a vtable at runtime.
  • Double dispatch — behavior depends on two objects' types (Visitor pattern).
  • Enables OCP — add new types without changing existing code that uses the base interface.
  • Constrained by LSP — subtypes must honor the base contract or polymorphism becomes unsafe.
Replace conditionals with polymorphism Long switch/if-else on a type code is often better modeled as subclasses each implementing the varying behavior.

Interview Questions

Filter

The ability for the same interface or method call to behave differently depending on the actual object type it's operating on — "one interface, many forms." It lets you write general code that works with many specific types.

Compile-time (static) — resolved by the compiler, via method overloading and generics/templates. Runtime (dynamic) — resolved at execution based on the object's actual type, via method overriding and virtual dispatch.

When subclasses override a base method, calling that method through a base-type reference runs the subclass's version at runtime. So one call site produces different behavior per actual object — runtime polymorphism.

Defining multiple methods with the same name but different parameter lists (number/types). The compiler picks which to call based on the arguments — a form of compile-time (ad-hoc) polymorphism.

A "draw" button in an app: pressing it draws whatever shape is selected — circle, line, or rectangle. Same action, different result depending on the object. Or a universal remote's "play" acting differently on a TV vs a stereo.

Subtype (runtime) polymorphism typically relies on inheritance or interface implementation: subtypes share a common base type/interface, override its methods, and are used interchangeably through that base type. The shared type provides the "one interface"; the overrides provide the "many forms."

You can write code that treats many types uniformly (e.g. loop over a list of shapes calling area()) and add new types later without changing that code. It reduces conditionals, improves extensibility, and supports the Open/Closed Principle.

The runtime mechanism that selects which method implementation to run based on the object's actual (runtime) type rather than its declared type. It's what makes overriding/runtime polymorphism work.

Static (early) binding resolves a call at compile time based on the declared type (overloaded methods, non-virtual/static/final methods). Dynamic (late) binding resolves at runtime based on the actual object type (overridden virtual methods). Overloading uses static binding; overriding uses dynamic binding. Understanding which applies explains why, e.g., an overloaded method chosen by a base-typed argument might not be the one you expect.

Ad-hoc: same name, type-specific implementations (overloading, operator overloading) — behavior chosen per argument types. Parametric: one implementation works uniformly over any type (generics/templates, e.g. List<T>, a generic max). Subtype (inclusion): code on a base type works with any subtype via overriding/dynamic dispatch. OOP interviews usually mean subtype polymorphism, but recognizing all three shows depth.

Writing code that operates uniformly on values of any type, with the type as a parameter — e.g. a Stack<T> or a generic swap(a, b) that works for any T. It gives type safety and reuse without duplicating code per type or resorting to casts. It's resolved at compile time (via type erasure in Java or monomorphization in C++/Rust) rather than runtime dispatch.

A form of polymorphism (in dynamic languages like Python/Ruby) where an object's suitability is determined by whether it has the needed methods/attributes, not by its declared type or inheritance — "if it walks like a duck and quacks like a duck, it's a duck." You can pass any object that supports the required behavior. It's flexible but shifts type errors to runtime and relies on convention/documentation rather than compile-time contracts.

Yes. Interfaces/protocols let unrelated classes be used polymorphically by implementing a common contract (no shared base class implementation). Duck typing achieves it structurally with no declared relationship. Higher-order functions and the Strategy pattern provide polymorphic behavior by passing different function/behavior objects. So polymorphism is about a common usable interface, which inheritance is only one way to provide.

Defining how built-in operators (+, ==, []) behave for your own types — e.g. + concatenating strings or adding vectors. It's ad-hoc polymorphism: the operator's meaning depends on operand types. Used well it makes value types (Money, Matrix, Complex) read naturally; abused, it hides costly or surprising behavior behind innocent-looking symbols.

Because overload resolution is based on the arguments at the call site, and a call like x = foo() or even foo(); doesn't unambiguously indicate which return type you want — the compiler can't decide. So Java/C++/C# require overloads to differ in parameters, not just return type. (Some languages with type inference in richer contexts can dispatch on expected return type, but mainstream OOP languages don't.)

A big switch/if-else on a type or "kind" field (e.g. handling each shape or payment type) can be replaced by subclasses/strategies that each implement the varying behavior, invoked polymorphically. The conditional collapses into a single call like obj.handle(). Benefits: adding a new case means adding a class, not editing (and risking) every switch; behavior for a type lives in one place; and it satisfies the Open/Closed Principle. This is the "replace conditional with polymorphism" refactoring.

Abstract methods declare a required operation on a base type/interface without implementing it, guaranteeing every concrete subtype provides its own version. This establishes the common polymorphic contract that callers rely on: they invoke the abstract method through the base type, and each subclass's implementation runs. They define "what all subtypes must do," which is the interface that polymorphism dispatches over.

Each polymorphic class has a virtual method table (vtable): an array of pointers to its (possibly overridden) virtual methods. Each object stores a hidden pointer to its class's vtable. A virtual call compiles to "load the vtable pointer, index to the method slot, call it," so the actual runtime type's implementation runs. Overriding just installs a different function pointer in the subclass's vtable. Interfaces add interface method tables (or itables). This indirection costs a pointer dereference and inhibits inlining, but enables dynamic dispatch. C++ makes it opt-in (virtual); Java virtual by default.

Double dispatch selects behavior based on the runtime types of two objects (e.g. collide(a, b) where the result depends on both a's and b's concrete types). Most OOP languages only do single dispatch (on the receiver), so a single virtual call can't vary on the argument's runtime type — you'd need type checks. The Visitor pattern solves it: a.accept(visitor) dispatches on a's type, then calls visitor.visitConcreteA(this), dispatching on the visitor's type — two virtual calls achieving double dispatch. It's the canonical workaround, at the cost of added boilerplate and coupling to the type set.

Type erasure (Java) compiles generics to a single implementation using the erased upper bound (usually Object), inserting casts; there's one copy of the code, generics don't exist at runtime (no new T(), limited reflection), and it can require boxing. Monomorphization (C++ templates, Rust, C#/.NET value-type generics) generates a specialized copy of the code per concrete type at compile time, giving full performance (no boxing, inlining, type info) at the cost of code bloat and longer compile times. The trade-off is uniform-but-slower/limited (erasure) vs specialized-fast-but-larger (monomorphization).

Virtual calls add an indirection (vtable lookup) and, more importantly, prevent the compiler from inlining the callee, which blocks further optimizations and hurts instruction cache/branch prediction in hot loops. Mitigations: JIT compilers perform devirtualization (if only one implementation is loaded, or via class-hierarchy analysis) and inline caches (remember the last resolved type at a call site) to speed monomorphic/polymorphic-but-stable call sites; developers can use final/sealed to hint non-overriding, use templates/generics for compile-time dispatch, or apply data-oriented design to avoid megamorphic hot paths. In most code the cost is negligible; it matters only in profiled tight loops.

OCP says software should be open for extension but closed for modification. Polymorphism provides the mechanism: clients depend on an abstract type and call its methods; you extend the system by adding a new subtype/implementation that plugs into the existing call sites, without editing them. E.g. adding a new PaymentMethod subclass requires no changes to the checkout code that iterates over PaymentMethod. The abstraction is the fixed extension point; new behavior arrives as new polymorphic types, so existing, tested code stays untouched.

Polymorphism assumes any subtype can safely stand in for the base type. If a subtype violates the Liskov Substitution Principle — strengthening preconditions, weakening postconditions, breaking invariants, throwing unexpected exceptions, or ignoring the intended behavior — then code written against the base type breaks when given that subtype. Callers are then forced to check concrete types and special-case them (defeating polymorphism) or suffer runtime bugs. So LSP is the correctness contract that makes substitution (hence polymorphism) safe; a common symptom of violation is a subclass that throws UnsupportedOperationException for an inherited method, signaling the hierarchy is wrong.

Type classes (Haskell) and traits (Rust) provide ad-hoc polymorphism by defining a set of operations a type can implement, decoupled from the type's definition — you can make an existing type conform after the fact, without inheritance or modifying it. Dispatch can be static (monomorphized, chosen by the concrete type at compile time) or dynamic (trait objects/dictionaries). Unlike OOP interfaces bundled into the class hierarchy, type classes/traits let conformance be added externally and support features like return-type-based dispatch and constrained generics (T: Ord). They offer interface-style polymorphism with more flexibility and often better performance (static dispatch by default in Rust).

Default methods let interfaces carry implementation, effectively allowing multiple inheritance of behavior — which reintroduces a diamond ambiguity: if a class implements two interfaces with the same default method, the compiler forces you to override and disambiguate (Interface.super.method()). They also blur the line between interface and abstract class, can create fragile-base-class-like issues (changing a default affects all implementers), and complicate reasoning about which implementation runs. Used judiciously (e.g. to evolve interfaces backward-compatibly or provide convenience methods), they're valuable; overused for shared state/behavior, they recreate multiple-inheritance complexity.

Start with code that switches on a type tag (e.g. switch(shape.type) computing area/perimeter/draw). Introduce a base type/interface with the varying operations, create a subclass per case, and move each branch's logic into the corresponding subclass method. Replace construction with a factory that maps input to the right subclass, then delete the switches — callers just invoke the polymorphic methods. Benefits: adding a new type touches one new class (OCP), each type's behavior is cohesive and localized, and the compiler can enforce that new types implement required methods. Trade-offs: more classes/indirection; behavior for one operation is now spread across many classes (the classic "expression problem" — polymorphism eases adding types but complicates adding operations, where a switch/visitor eases adding operations but complicates adding types). Choose based on which axis (types vs operations) changes more often.

The ability to use an object of a subtype anywhere its base type is expected, with the subtype's overridden behavior running — the most common OOP meaning of "polymorphism."

Method hiding happens with static (non-virtual) methods or when a subclass declares a method that isn't actually overriding a virtual one — the version called depends on the declared (compile-time) type, not the runtime object. Overriding uses the runtime type (dynamic dispatch). So with hiding, Base b = new Derived(); b.m(); may call Base's version, which surprises people expecting polymorphism. Fields and static methods are hidden, not overridden; instance virtual methods are overridden.

No — in most languages only methods are dispatched polymorphically. Fields are resolved by the declared (compile-time) type and are hidden, not overridden, if redeclared in a subclass. This is why you expose state through overridable getter methods rather than relying on field access when you need polymorphic behavior: a subclass can override the getter but can't "override" a field's value polymorphically.

The expression problem is the difficulty of extending a system along two axes — adding new types and adding new operations — without modifying existing code or losing type safety. Object-oriented polymorphism makes adding a new type easy (implement the interface) but adding a new operation hard (must edit every class). Functional/pattern-matching or the Visitor pattern makes adding an operation easy but adding a type hard (must edit every operation/visitor). Solutions like type classes, multimethods, or clever combinations aim to allow both; recognizing which axis changes more often guides whether to lean on polymorphism (types vary) or visitors/pattern matching (operations vary).