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.
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.
singledispatchresolves 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 needstd::visitover astd::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
Protocolif 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
isinstancetests 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_checkablecompares method names, not signatures, so a type checker run against theProtocolor an ABC that fails at construction catches more.