← Master Index
Vol. 03 Module 3.2 Lecture

Polymorphism

Object-Oriented Programming

How This Lesson Fits the Module

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

Definition — Polymorphism

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
Production Relevance

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

Misconception 1: “Polymorphism requires inheritance.”

Reality: In Python, unrelated classes are polymorphic if they implement the same methods. Inheritance helps organize code but is not required for duck typing.

Misconception 2: “Changing the model always requires rewriting training code.”

Reality: If you code against interfaces (model(x), dataset[i]), swapping implementations is a one-line change.

Quick Knowledge Check

  1. Short Answer: What is duck typing? Answer: Using objects based on the methods they implement, not their explicit class type.
  2. True/False: train_epoch must know the concrete class of model to call it. Answer: False—it only needs model(inputs) and model.train() to exist.
  3. Short Answer: How do sklearn Pipelines benefit from polymorphism? Answer: Each step is an estimator with fit/transform; Pipeline chains any compatible steps.
  4. Short Answer: Define polymorphism in one phrase. Answer: Same interface, different implementations.
  5. True/False: Polymorphism in Python requires inheritance. Answer: False—unrelated classes are polymorphic if they implement the same methods (duck typing).
  6. Multiple Choice: evaluate_estimator works on LogisticRegression and RandomForest because both: (a) share memory, (b) implement fit/score, (c) use GPUs, (d) inherit nn.Module. Answer: (b).
  7. Short Answer: What dataset methods let one DataLoader handle images, text, or tabular data? Answer: __len__ and __getitem__.
  8. True/False: Python chooses the method implementation at runtime based on the object’s type. Answer: True.
  9. 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).
  10. Short Answer: Name one PyTorch interface that lets the same loop train CNNs or transformers. Answer: forward / __call__ plus parameters() (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.
Trainer Guide

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?

What’s Next Continue to Encapsulation to control what callers can access inside your dataset and model objects.