Polymorphism6 min · 11 of 16

Overriding, overloading and duck typing

Separate the three mechanisms filed under polymorphism by when each resolves, and pick between a Protocol, an ABC and a Java interface by where failure lands.

Three different mechanisms get filed under "polymorphism", and interviews trade on the confusion. Overriding picks a method body at run time from the receiver. Overloading picks a signature at compile time from the argument types. Duck typing skips the declaration step entirely and asks only whether the attribute is there when the line runs.

They resolve at different moments, they fail in different places, and one of them does not exist in Python at all.

Overriding: the receiver decides, while the program runs

A subclass method with the same name and parameter list replaces the parent's entry for that signature, and the object on the left of the dot chooses which entry is used. That is the mechanism in dynamic dispatch: a fixed slot, a table chosen by the object. The name alone is not enough: a subclass String render(int n) matches no inherited signature, so it overloads rather than overrides. Python has no signatures to match against, so there the name is the whole key.

class Renderer:
    def render(self, doc: str) -> str:
        return doc

class HtmlRenderer(Renderer):
    def render(self, doc: str) -> str:
        return f"<p>{doc}</p>"
class Renderer {
    String render(String doc) { return doc; }
}

class HtmlRenderer extends Renderer {
    @Override String render(String doc) { return "<p>" + doc + "</p>"; }
}

@Override is not decoration: it makes the compiler check that something is actually being overridden, so both rnder and that render(int n) become compile errors rather than new methods that never run. Python has no such check. Misspell the name in a subclass and you have added a method, the parent's version keeps being called, and the only symptom is behaviour that quietly did not change.

Overloading: the compiler decides, before the program runs

Java lets several methods share a name with different parameter lists, and resolves between them from the static types of the arguments at the call site. The chosen signature is fixed in the bytecode.

static void print(Object o) { System.out.println("object"); }
static void print(String s) { System.out.println("string"); }

Object o = "hello";
print(o);      // prints "object"

o holds a string, and print(Object) runs anyway, because overload resolution never looks at run-time types. Put both mechanisms in one line and both show: in r.render(doc) the signature is chosen from the declared type of doc at compile time, and the body from the actual class of r at run time.

Two selections, two different times. Overloading is finished before the program starts; overriding has not begun.
Overload resolution happens at compile time from the argument types; override dispatch happens at run time from the receiverCompile timeRun timeoverloadbaked inreceiverreceiverr.render(doc)one line of sourcePick asignaturestatic type of docOne call siterender(Doc) is fixedHtmlRendererthe receiver todayPdfRendererthe receiver tomorrow

Scroll to zoom · drag to pan · 0 fits · Esc closes

C++ overloads too, on all argument types, with a resolution algorithm elaborate enough for its own chapter. What it still lacks is run-time dispatch on more than the receiver: for that you reach for std::variant and std::visit.

Why Python has no overloading

Not an omission — a consequence of what def does. def binds a name in a namespace. A second def with the same name rebinds it, and the first function object is gone.

class Report:
    def render(self, doc): ...
    def render(self, doc, theme): ...   # the first one no longer exists

r.render(doc) now raises TypeError for a missing argument. There is nothing to resolve between at call time because only one function survived the class body. Three replacements, in the order to reach for them:

Default and variadic arguments. One function, one signature, branch inside. This covers most of what overloading is used for and keeps one place to read.

functools.singledispatch, when the branching is genuinely on type:

from functools import singledispatch

@singledispatch
def to_json(value):
    raise TypeError(f"no rule for {type(value).__name__}")

@to_json.register
def _(value: int) -> str:
    return str(value)

@to_json.register
def _(value: list) -> str:
    return "[" + ", ".join(to_json(v) for v in value) + "]"

This is not Java's overloading wearing a hat. Java resolves on the static types of every argument before the program runs; singledispatch resolves on the run-time type of the first argument only, while it runs, through a dict keyed by type that falls back to the MRO and caches the result. That is a hash lookup per call, order 100 ns on the cost model from hash maps, which is the same order as the attribute lookup that got you there. Use singledispatchmethod for the method form.

typing.overload declares several signatures for a type checker and has no run-time effect at all. The stubs are discarded; one implementation still does the work. It documents an API, it does not dispatch.

Duck typing versus nominal typing

Python asks whether the attribute exists at the moment of the call. Java asks whether the type was declared to have it, once, at compile time. The same call, both ways:

def render_all(items):
    return "\n".join(item.render() for item in items)
interface Renderable { String render(); }

static String renderAll(List<Renderable> items) {
    return items.stream().map(Renderable::render).collect(joining("\n"));
}

The Python version accepts anything with a render method: your class, a test double, a third-party class you cannot edit. The Java version accepts only what was declared implements Renderable; a third-party class with a byte-identical render() does not qualify, and you write an adapter.

What each side is buying is the position of the failure. Give render_all a list of 1,000 items where item 900 has no render: Python has already produced 899 outputs, along with whatever writes or emails those caused, before AttributeError arrives. The expectation was never written down, so nothing could have checked it earlier. Java's version could not have compiled with that list at all.

Python has both halves available, and they differ in where they catch it:

from typing import Protocol

class Renderable(Protocol):
    def render(self) -> str: ...

A Protocol is structural: the third-party class still qualifies with no inheritance and no adapter, and a type checker verifies the call before the process starts. abc.ABC with @abstractmethod is nominal: implementers must inherit, and a subclass missing a method raises TypeError when you construct it — construction time, not item 900.

Pick by ownership. Protocol when the consumer defines the expectation and the implementers are not yours. ABC when you own the hierarchy and want a missing method to stop construction. Checking at a boundary is cheap: 1,000 isinstance tests at order 100 ns is 0.1 ms, against a datacenter round trip of 0.5 ms. runtime_checkable makes isinstance work on a Protocol, though it checks method names only, never signatures.

In an interview

"Does Python support method overloading?" separates people quickly. The answer that scores is the mechanism, not the verdict: "No — a second def rebinds the name, so there is only ever one function to call. For type-based branching I would use functools.singledispatch; for optional parameters, defaults."

For "overriding versus overloading", give the two axes rather than the labels: run time versus compile time, and receiver versus arguments. Then the Java Object o = "hello" case, which shows you have actually hit it.

On duck typing, the answer that reads as experience names the cost: "it lets me pass a class I do not own, and it moves the check to the point of use. In Python I would write a Protocol so the expectation is in the signature without forcing anyone to inherit from me."

The mistake that loses points: calling overloading "compile-time polymorphism" and stopping there. The phrase is correct and says nothing about what gets chosen. Second mistake: treating duck typing as nominal typing with less ceremony, rather than a trade of an earlier check for a wider set of inputs.

Check yourself

You need to_json to branch on the run-time type of its second argument. Which mechanism in this lesson does that, and what do you write instead?

None. singledispatch resolves at run time but cannot consider any argument but the first, while Java resolves on the full parameter list. Java's selection on the second argument is still made at compile time from declared types, so neither gives you run-time dispatch on it — for that you need std::visit over a std::variant, a visitor, or an explicit two-level dispatch.

A third-party class has the render() method you need and you cannot edit it. How do you accept it in Python, and in Java?

Python: pass it, and declare a Protocol if you want the type checker to verify the shape — Protocols are structural, so no inheritance is required. Java: write an adapter that implements your interface and delegates. A nominal system cannot be told after the fact that an existing type qualifies.

A handler calls render() on 10,000 items and every render sends an email. Do you screen the list against a runtime_checkable Protocol first, and what does that cost?

Screen it: 10,000 isinstance tests at order 100 ns is about 1 ms, two datacenter round trips, against a failure at item 900 with 899 emails already gone. Note the limit — runtime_checkable compares method names, not signatures, so a type checker run against the Protocol or an ABC that fails at construction catches more.