29 · Polymorphism and duck typing
Same method name, different behaviour — how Python's duck typing lets any object slot into a pipeline as long as it has the right methods.
**Polymorphism** means the same function call works on different types because each type implements the expected method. Python takes this further with **duck typing**: if an object has `.predict()`, it can go into any `evaluate()` function that calls `.predict()` — no shared base class required. "If it walks like a duck and quacks like a duck, it's a duck."
Without this:
Without polymorphism you'd write a separate `evaluate_linear`, `evaluate_quadratic`, `evaluate_rf` function for every model type. With it, one `evaluate(model, xs)` works for all — the entire sklearn pipeline API is built on this insight.
In a classical language (Java, C++) polymorphism requires a common base class or interface. In Python it's simpler: if you call model.predict(x) and the object has a .predict method, Python runs it — regardless of the object's type. This is duck typing.
The evaluate function below works with any object that has a .predict(x) method: a linear model, a quadratic model, a random forest, a hand-rolled rule engine. No inheritance required. This is exactly how sklearn's Pipeline.fit_transform works — it calls .fit(X) and .transform(X) on every step without caring whether each step inherits from a specific base class.
isinstance() is occasionally useful (guarding against genuinely incompatible types), but reaching for it too often is a code smell — it means you're fighting duck typing instead of embracing it. The modern alternative for type-checker support is typing.Protocol: declare the interface you need (the required method signatures) and the type checker verifies any object that passes the protocol.
Python (in browser)
`evaluate` never checks `type(model)` or `isinstance(model, SomeBase)` — it just calls `.predict(x)`. Python will raise `AttributeError` if the method is missing, which is a clear signal that the contract wasn't met. This is idiomatic; the contract is documented by convention, not enforced by the type system at runtime.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
`hasattr(obj, 'predict')` checks whether the method exists without requiring a specific type. This is the duck-typing-friendly guard. `isinstance()` is better reserved for genuine type dispatch (e.g. handling both `str` and `bytes` input paths).
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
`Protocol` defines a **structural** interface: any class that has `predict(self, x: float) -> float` satisfies `Predictor` at type-check time — no explicit inheritance required. This is the modern way to express duck typing in type-annotated code. Compare to Java's `interface Predictor` which requires `implements` at the call site.
What does "duck typing" mean in Python?
- Polymorphism: same method name, different behaviour per class. Python achieves this via duck typing — if the method exists, it works, regardless of the class hierarchy.
- `hasattr(obj, 'method_name')` is the duck-typing-friendly guard. `isinstance()` is occasionally useful but can be a smell if overused — it fights duck typing.
- `typing.Protocol` lets you express a structural interface for type checkers without requiring explicit inheritance at the call site.
Sklearn's `.fit()` / `.predict()` / `.transform()` / `.score()` interface is a duck-typed contract — any object that implements these methods works in a `Pipeline` or `cross_val_score` call. HuggingFace Transformers exposes `.generate()` across dozens of model architectures (GPT-2, T5, BLOOM) with the same call signature — callers never check which architecture they have. PyTorch's `DataLoader` calls `dataset.__len__()` and `dataset.__getitem__(idx)` on whatever you pass as `dataset`.
If you remove it: Without duck typing, sklearn pipelines would require every step to explicitly subclass a base class — the community ecosystem of drop-in transformers and estimators would be an order of magnitude smaller.