Production OOP Patterns in ML: Interview Reference


Table of Contents

  1. Classes and Objects
  2. Encapsulation: Hide Implementation, Expose Interface
  3. Inheritance: Reuse Code Across Models
  4. Polymorphism: Same Interface, Different Behavior
  5. Abstraction with ABC: Enforce the Contract
  6. Duck Typing vs. ABC
  7. Method Overriding
  8. Composition: Building Complex Objects
  9. Attributes: Data in Objects
  10. Namespace and Scope
  11. Quick Reference
  12. Interview Talking Points
  13. Decision Tree: When to Use Each Concept
  14. Key Takeaway

Classes and Objects

What is a Class?

A class is a blueprint for creating objects. It defines:

  • Attributes (data the object holds)
  • Methods (functions the object can perform)

What is an Object?

An object is a concrete instance of a class. Multiple objects can exist from the same class, each with different data.

Production Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Model:
    def __init__(self, model_id: str):
        self.model_id = model_id      # attribute
        self.is_fitted = False         # attribute
    
    def fit(self, X, y):              # method
        pass
    
    def predict(self, X):             # method
        pass

# Objects: three different prediction systems
fraud_detector = Model(model_id="fraud_xgb")
ctr_predictor = Model(model_id="ctr_nn")
budget_forecaster = Model(model_id="forecast_arima")

# Each trains separately
fraud_detector.fit(X_fraud, y_fraud)

Why it matters: Classes enforce structure. Every model follows the same .fit() + .predict() interface. KServe expects this contract.


Encapsulation: Hide Implementation, Expose Interface

What is Encapsulation?

Bundle data (attributes) and behavior (methods) in a single unit, controlling what users can access.

The Problem

Without encapsulation, callers repeat internal logic:

1
2
3
4
5
6
# Fragile: every caller does preprocessing
model = xgb.XGBClassifier()
model.load_model("model.pkl")
features = raw_input[["age", "income"]]
features = scaler.transform(features)
prediction = model.predict_proba(features)

Risk: One mistake propagates everywhere.

The Solution

Hide preprocessing inside the class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class PredictionServer:
    def __init__(self, model_path: str):
        self._model = xgb.XGBClassifier()         # private
        self._model.load_model(model_path)
        self._scaler = joblib.load("scaler.pkl") # private
    
    def predict(self, raw_input: dict) -> dict:  # public
        """User-facing interface."""
        features = self._preprocess(raw_input)
        score = self._model.predict_proba(features)[0, 1]
        return {"fraud_prob": score}
    
    def _preprocess(self, raw_input):             # private
        """Implementation detail."""
        features = pd.DataFrame([raw_input])
        return self._scaler.transform(features)

# Caller only uses public method
server = PredictionServer("models/fraud.pkl")
result = server.predict({"age": 35, "income": 100000})
# Result: {"fraud_prob": 0.12}

Why it matters: If we change preprocessing tomorrow, we update once inside _preprocess(), not in 50 services.

Private vs. Public convention:

  • public_method() — designed for external use
  • _private_method() — internal implementation, users shouldn’t call this

Inheritance: Reuse Code Across Models

What is Inheritance?

A child class inherits attributes and methods from a parent class, reducing duplication.

The Pattern

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class BaseEstimator:
    def __init__(self, model_id: str):
        self.model_id = model_id
    
    def _validate_input(self, X):
        """Shared validation logic."""
        if X.shape[1] != len(self.feature_names):
            raise ValueError("Feature mismatch")


class XGBoostModel(BaseEstimator):
    def fit(self, X, y):
        self._model = xgb.XGBClassifier()
        self._model.fit(X, y)
    
    def predict(self, X):
        self._validate_input(X)  # inherited method
        return self._model.predict_proba(X)


class CatBoostModel(BaseEstimator):
    def fit(self, X, y):
        self._model = catboost.CatBoostClassifier()
        self._model.fit(X, y)
    
    def predict(self, X):
        self._validate_input(X)  # inherited method
        return self._model.predict_proba(X)

Why it matters: Write _validate_input() once. Both XGBoost and CatBoost inherit it. No duplication.


Polymorphism: Same Interface, Different Behavior

What is Polymorphism?

Different classes respond to the same method call in different ways.

The Pattern

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class EnsembleModel:
    def __init__(self, models: list):
        self.models = models  # can contain any model type
    
    def fit(self, X, y):
        for model in self.models:
            model.fit(X, y)  # polymorphic call
            # XGBoost.fit() works differently than CatBoost.fit()
            # But both respond to the same interface
    
    def predict(self, X):
        predictions = []
        for model in self.models:
            predictions.append(model.predict(X))  # polymorphic call
        return np.mean(predictions, axis=0)


# Usage
xgb_model = XGBoostModel()
cat_model = CatBoostModel()
lgb_model = LightGBMModel()

ensemble = EnsembleModel([xgb_model, cat_model, lgb_model])
ensemble.fit(X_train, y_train)
scores = ensemble.predict(X_test)

Why it matters: Ensemble doesn’t care what’s inside. Drop in a new model type without touching ensemble code.


Abstraction with ABC: Enforce the Contract

Why ABC Exists

In Python, everything is dynamic. Without enforcement, you can create a class that looks complete but misses required methods.

1
2
3
4
5
# Without ABC, nothing stops this
class BadModel:
    def fit(self, X, y):
        print("Training")
    # Missing predict() — bug discovered at runtime

ABC fixes this by: Preventing incomplete subclasses from being instantiated.

How ABC Works

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from abc import ABC, abstractmethod

class ModelContract(ABC):
    @abstractmethod
    def fit(self, X, y):
        pass
    
    @abstractmethod
    def predict(self, X):
        pass


# This works — implements all abstract methods
class GoodModel(ModelContract):
    def fit(self, X, y):
        self._model = xgb.XGBClassifier()
        self._model.fit(X, y)
    
    def predict(self, X):
        return self._model.predict_proba(X)

model = GoodModel()  # OK


# This fails — missing predict()
class BadModel(ModelContract):
    def fit(self, X, y):
        pass
    # Missing predict()

model = BadModel()  # TypeError: Can't instantiate abstract class BadModel

Error timing matters: With ABC, you fail at class definition time, not at runtime.

When to Use ABC

Use CaseDecision
Small scripts, one-off analysisDon’t use ABC
Production system, multiple teamsUse ABC
Framework design, plug-and-play systemUse ABC
Large codebase, implicit contracts become chaosUse ABC

Duck Typing vs. ABC

What is Duck Typing?

“If it walks like a duck and quacks like a duck, it’s a duck.”

Python doesn’t check type — it checks capability.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Dog:
    def speak(self):
        return "Woof"

class Human:
    def speak(self):
        return "Hello"

def talk_to(entity):
    print(entity.speak())  # Python never checks if entity is Dog or Human
    # It just calls .speak() and trusts it works

talk_to(Dog())      # Works
talk_to(Human())    # Works
talk_to(123)        # Fails at runtime: 'int' has no attribute 'speak'

Duck Typing Characteristics

  • Implicit contract — no formal requirement
  • Flexible — add new types easily
  • Risky at scale — bugs discovered at runtime

ABC Characteristics

  • Explicit contract — formal requirement
  • Enforced — bugs caught at instantiation time
  • Rigid — requires inheritance from ABC

When to Use Which?

Duck Typing works when:

  • Code is small and well-understood
  • Team is small
  • Requirements are stable

ABC works when:

  • Multiple teams contribute
  • Requirements change frequently
  • You want strict architectural discipline
  • Production reliability matters

Hybrid Approach (Best for ML)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from abc import ABC, abstractmethod

class MLModel(ABC):
    """Enforces core contract for all models."""
    @abstractmethod
    def fit(self, X, y):
        pass
    
    @abstractmethod
    def predict(self, X):
        pass


class XGBoostBidder(MLModel):
    """Concrete implementation."""
    def fit(self, X, y):
        self._model = xgb.XGBClassifier()
        self._model.fit(X, y)
    
    def predict(self, X):
        return self._model.predict_proba(X)
    
    def explain(self):
        """Extra method: duck typing allows this."""
        return self._model.feature_importances_


# Usage respects ABC contract + ducks type on explain()
model = XGBoostBidder()
model.fit(X, y)
model.predict(X_test)
model.explain()  # optional — duck typing

Why this works: Core contract is enforced (ABC). Additional methods are flexible (duck typing).


Method Overriding

What is Method Overriding?

A subclass defines a method with the same name as a parent class method, replacing the parent’s implementation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class BaseModel:
    def validate(self, X):
        """Base validation."""
        if X is None:
            raise ValueError("X cannot be None")


class RobustModel(BaseModel):
    def validate(self, X):
        """Override: more strict validation."""
        super().validate(X)  # call parent logic first
        if X.shape[0] < 100:
            raise ValueError("Minimum 100 samples required")

Why it matters: Subclasses specialize parent behavior without breaking the interface.


Composition: Building Complex Objects

What is Composition?

Include instances of other classes as attributes within a class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class PredictionPipeline:
    def __init__(self, preprocessor, model, postprocessor):
        self.preprocessor = preprocessor    # composed objects
        self.model = model
        self.postprocessor = postprocessor
    
    def predict(self, X):
        X = self.preprocessor.transform(X)
        pred = self.model.predict(X)
        pred = self.postprocessor.transform(pred)
        return pred

Why it matters: More flexible than inheritance. You can swap components at runtime.


Attributes: Data in Objects

What are Attributes?

Variables that belong to an object and describe its state.

1
2
3
4
5
6
class Model:
    def __init__(self, model_id: str, version: str):
        self.model_id = model_id        # attribute
        self.version = version           # attribute
        self.trained_at = None           # attribute
        self.metrics = {}                # attribute

Why it matters: Track model metadata (version, timestamp, performance). Enables versioning and rollbacks.


Namespace and Scope

What is a Namespace?

A mapping of names → objects. Think: a dictionary of identifiers.

1
2
3
4
5
6
7
# Each scope has its own namespace
def train_model():
    X = load_data()        # X is in local namespace
    y = load_labels()      # y is in local namespace
    return model

# After function returns, X and y are removed from that namespace

What is Scope?

The region of code where Python will look for a name.

LEGB Resolution Rule

Python searches for a name in this order:

  1. Local (inside current function)
  2. Enclosing (in outer function, for nested functions)
  3. Global (module-level)
  4. Built-in (Python’s built-ins like print, len)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
x = 10  # Global scope

def outer():
    x = 20  # Enclosing scope (for inner function)
    
    def inner():
        x = 30  # Local scope
        print(x)  # Prints 30 (Local)
    
    inner()
    print(x)  # Prints 20 (Enclosing)

print(x)  # Prints 10 (Global)

Why it matters: Understanding scope prevents variable shadowing bugs in nested class methods.


Quick Reference

TermMeansExample
ClassTemplate for objectsclass Model
ObjectInstance of a classfraud_detector = Model()
AttributeData in an objectmodel.version
MethodFunction in a classmodel.fit()
InheritanceChild reuses parentXGBoostModel(BaseModel)
EncapsulationHide internals, expose interfacedef predict() public, _validate() private
PolymorphismSame interface, different behaviorAll models respond to .fit() and .predict()
Abstraction (ABC)Enforce required methods@abstractmethod forces implementation
Duck TypingCheck capability, not typeIf it has .speak(), call it
CompositionInclude other objects as attributesPipeline(model1, model2, model3)
Method OverridingSubclass replaces parent methodRobustModel.validate() overrides BaseModel.validate()

Interview Talking Points

“Tell me about inheritance in your production systems.”

“Every model at Adform—fraud, RTB, forecasting—inherits from BaseEstimator that enforces .fit(), .predict(). This ensures consistency across domains. When we deploy to KServe, the container expects this interface. Inheritance reduces boilerplate ~60%.”

“How do you handle incomplete implementations?”

“We use abstract base classes (ABC) with @abstractmethod. If a model skips .predict(), the code fails at instantiation—we catch bugs at definition time, not runtime. In production, that discipline matters.”

“Duck typing vs. ABC—which do you prefer?”

“At small scale, duck typing is flexible. At Adform’s scale, ABC enforces architectural discipline. Core contract is ABC (.fit(), .predict()). Optional methods are duck typing (.explain(), .get_metadata()). Hybrid approach.”

“How does encapsulation help in production?”

“Every model deployed to KServe is wrapped with a .predict() method that handles scaling, validation, fallback, logging. Teams call one method. If we upgrade preprocessing tomorrow, we change it once inside the wrapper, not in 50 places.”

“Tell me about polymorphism in your work.”

“In our RTB simulator, different bidding strategies (fixed, learned, dynamic) all inherit from BiddingStrategy. The loop calls .generate_bid() without knowing which strategy runs. We A/B tested three new strategies by dropping them into the same harness.”


Decision Tree: When to Use Each Concept

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Building a model class?
├─ Will multiple teams use it?
│  └─ YES → Use ABC (enforce contract)
│  └─ NO → Use duck typing (stay flexible)
├─ Does it need internal preprocessing?
│  └─ YES → Use encapsulation (hide internals)
├─ Will you have multiple model types?
│  └─ YES → Use inheritance + polymorphism (same interface, different behavior)
└─ Will you combine multiple models?
   └─ YES → Use composition (include other objects as attributes)

Key Takeaway

OOP solves production problems:

  • Classes enforce structure and contracts
  • Inheritance reduces boilerplate across models
  • Encapsulation prevents breaking changes
  • Polymorphism enables safe composition
  • ABC enforces discipline; duck typing enables flexibility
  • Composition is more flexible than inheritance for complex systems

Not academic—it makes codebases maintainable at scale.