Inheritance created families of related classes. Polymorphism is the payoff: code written against a common interface works with any conforming object—swap logistic regression for a random forest, ResNet for ViT, CSV dataset for image dataset—without rewriting the training loop.
This is how experiment frameworks, AutoML tools, and production serving layers stay flexible. One train(model, loader) function; many model classes.
Learning Objectives
By the end of this lesson, students should be able to:
- Define polymorphism as “same interface, different implementations.”
- Write functions that accept any object implementing required methods (duck typing).
- Swap model or dataset implementations without changing caller code.
- Explain how polymorphism enables sklearn Pipelines and PyTorch model interchange.
- Distinguish compile-time polymorphism (not Python’s model) from runtime method dispatch.
- Design a small trainer function that works across multiple model wrappers.
What Polymorphism Means
Polymorphism (“many forms”) means different classes can expose the same method names and be used interchangeably by code that depends only on the interface—not the concrete class. The correct method implementation is chosen at runtime based on the object’s type.
Python uses duck typing: “If it walks like a duck and quacks like a duck, treat it as a duck.” No explicit interface keyword is required—if an object has fit and predict, it can play the role of an estimator.
Polymorphic Training Function
Define a trainer that works with any model object providing forward-like behavior and any iterable of batches:
def train_epoch(model, dataloader, optimizer, criterion):
"""Works with ANY model and dataloader matching this interface."""
model.train()
total_loss = 0.0
for inputs, targets in dataloader:
optimizer.zero_grad()
outputs = model(inputs) # polymorphic call
loss = criterion(outputs, targets)
loss.backward()
optimizer.step()
total_loss += loss.item()
return total_loss / len(dataloader)
Whether model is MLPClassifier, nn.Linear, or a Hugging Face wrapper—if model(inputs) returns predictions and supports .train(), the loop unchanged.
Swapping Implementations
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
def evaluate_estimator(estimator, X_train, y_train, X_test, y_test):
estimator.fit(X_train, y_train) # polymorphic: fit exists on both
return estimator.score(X_test, y_test)
# Same function, different algorithms
lr_acc = evaluate_estimator(LogisticRegression(), X_train, y_train, X_test, y_test)
rf_acc = evaluate_estimator(RandomForestClassifier(), X_train, y_train, X_test, y_test)
| Context | Shared Interface | Polymorphic Benefit |
|---|---|---|
| scikit-learn | fit, predict, score |
GridSearch, Pipelines, cross-validation work on any estimator |
| PyTorch | forward / __call__, parameters |
Same training loop for CNNs, transformers, custom heads |
| Datasets | __len__, __getitem__ |
One DataLoader handles images, text, tabular data |
| Model serving | predict(payload) |
API route delegates to any backend implementing the contract |
Operator Overloading as Polymorphism
Python’s dunder methods enable polymorphic syntax. Tensors, NumPy arrays, and scalars all respond to + and * through __add__ and __mul__:
import torch
a = torch.tensor([1.0, 2.0])
b = torch.tensor([3.0, 4.0])
c = a + b # dispatches to Tensor.__add__ — same syntax, type-specific logic
Model Wrapper Pattern
Wrappers make heterogeneous models polymorphic under one API:
class SklearnModelWrapper:
def __init__(self, sklearn_estimator):
self.estimator = sklearn_estimator
def predict(self, features):
return self.estimator.predict(features)
class PyTorchModelWrapper:
def __init__(self, torch_model):
self.torch_model = torch_model
def predict(self, features):
import torch
self.torch_model.eval()
with torch.no_grad():
tensor = torch.tensor(features, dtype=torch.float32)
return self.torch_model(tensor).numpy()
def serve(wrapper, features):
return wrapper.predict(features) # polymorphic — doesn't care which backend
Model registries and inference servers expose a uniform predict endpoint. Polymorphism—via wrappers or shared base classes—lets teams deploy sklearn, PyTorch, or ONNX models behind the same API.
Polymorphism vs. Type Hints
Python does not enforce interfaces at runtime. Type hints (covered later in Module 3.4) document expected shapes:
from typing import Protocol
class Predictor(Protocol):
def predict(self, features): ...
def serve(model: Predictor, features):
return model.predict(features)
Static checkers verify conformance; at runtime, duck typing still rules.
Common Misconceptions
Reality: In Python, unrelated classes are polymorphic if they implement the same methods. Inheritance helps organize code but is not required for duck typing.
Reality: If you code against interfaces (model(x), dataset[i]), swapping implementations is a one-line change.
Quick Knowledge Check
- Short Answer: What is duck typing? Answer: Using objects based on the methods they implement, not their explicit class type.
- True/False:
train_epochmust know the concrete class ofmodelto call it. Answer: False—it only needsmodel(inputs)andmodel.train()to exist. - Short Answer: How do sklearn Pipelines benefit from polymorphism? Answer: Each step is an estimator with
fit/transform; Pipeline chains any compatible steps. - Short Answer: Define polymorphism in one phrase. Answer: Same interface, different implementations.
- True/False: Polymorphism in Python requires inheritance. Answer: False—unrelated classes are polymorphic if they implement the same methods (duck typing).
- Multiple Choice:
evaluate_estimatorworks on LogisticRegression and RandomForest because both: (a) share memory, (b) implementfit/score, (c) use GPUs, (d) inheritnn.Module. Answer: (b). - Short Answer: What dataset methods let one DataLoader handle images, text, or tabular data? Answer:
__len__and__getitem__. - True/False: Python chooses the method implementation at runtime based on the object’s type. Answer: True.
- Multiple Choice: Best way to swap models without rewriting training code: (a) hard-code class names, (b) code against
model(x)/dataset[i]interfaces, (c) copy-paste loops, (d) use only globals. Answer: (b). - Short Answer: Name one PyTorch interface that lets the same loop train CNNs or transformers. Answer:
forward/__call__plusparameters()(or.train()).
Key Takeaways
- Polymorphism lets one function work with many classes through a shared interface.
- Python achieves this via duck typing and runtime method dispatch.
- ML frameworks standardize interfaces:
fit/predict,forward,__getitem__. - Model wrappers unify disparate backends under one serving API.
- Design training and evaluation code against interfaces, not concrete classes.
Hands-on: Run the same evaluate_estimator function on LogisticRegression and RandomForest. Then write a tiny wrapper so a dummy PyTorch module can plug into a train_epoch loop that only calls model(x) and model.train().
Discussion: When would you still prefer an ABC over pure duck typing for production trainers?