Interview Insights · ML Engineer Code
Why Every ML Engineer Should Practice Explaining Their Code Aloud
Quick summary — Explaining ML code
Explaining ML code aloud shows whether you understand it or only know that it works. In code rounds and project deep dives, interviewers often point at a single line and ask why it is there. Lines like zero_grad, model.eval, torch.no_grad, fit versus transform, or a copy inside a function each guard against a specific bug. Practising aloud at four levels, what it does, why, what breaks without it and what the alternative is, turns familiar code into code you can defend.
In this guide you will learn:
- The four levels of explaining any line of ML code
- A PyTorch training loop explained line by line
- A custom scikit-learn transformer and why fit and transform are separate
- How to talk through a code review of your own ML code
- Drills to practise line-level explanations
SECTION 01Explaining a PyTorch training loop line by line
Here 'code' means the scripts and modules in your ML project: training loops, preprocessing classes, data loaders and helper functions. Not a notebook story, but the actual lines. The PyTorch training loop is the most common place interviewers probe, because almost every line exists to prevent a specific bug, and the bugs are often silent.
For example, if you forget optimizer.zero_grad(), PyTorch adds new gradients to the old ones, so each step uses a growing sum. Training may still run and the loss may still move, which is why it is so dangerous. A candidate who can explain that has clearly understood the code, not just copied it.
| Line | What it does | If you remove it | Is the failure loud or silent? |
|---|---|---|---|
| optimizer.zero_grad() | Clears gradients from the previous step | Gradients accumulate across batches | Silent, training just behaves strangely |
| model.train() / model.eval() | Switches dropout and batch norm behaviour | Validation runs with dropout active | Silent, scores are noisy or lower |
| with torch.no_grad(): | Stops gradient tracking in evaluation | Extra memory use, slower validation | Usually silent, sometimes out-of-memory |
| loss.item() | Converts a tensor to a Python number | Summing tensors keeps the graph alive | Memory grows over epochs |
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
torch.manual_seed(0)
X = torch.randn(1000, 10)
y = (X[:, 0] + X[:, 1] > 0).float().unsqueeze(1)
train_dl = DataLoader(TensorDataset(X[:800], y[:800]), batch_size=32, shuffle=True)
val_dl = DataLoader(TensorDataset(X[800:], y[800:]), batch_size=64)
model = nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Dropout(0.2), nn.Linear(32, 1))
loss_fn = nn.BCEWithLogitsLoss()
opt = torch.optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(5):
model.train()
total = 0.0
for xb, yb in train_dl:
opt.zero_grad()
loss = loss_fn(model(xb), yb)
loss.backward()
opt.step()
total += loss.item() * len(xb)
model.eval()
correct = 0
with torch.no_grad():
for xb, yb in val_dl:
pred = (torch.sigmoid(model(xb)) > 0.5).float()
correct += (pred == yb).sum().item()
print(f"epoch {epoch} train_loss={total/800:.3f} val_acc={correct/200:.3f}")"Each epoch has a training half and an evaluation half.
model.train() turns dropout on. For each batch I clear old
gradients with zero_grad, because PyTorch adds gradients by
default. Then forward pass, loss, backward to compute
gradients, and step to update the weights.
I use BCEWithLogitsLoss, so the model outputs raw logits and
the loss applies the sigmoid internally, which is more
numerically stable than a separate sigmoid plus BCELoss.
loss.item() gives a plain float, so I don't keep the graph
alive while summing.
For validation, model.eval() turns dropout off and no_grad
stops gradient tracking, which saves memory and time."
Notice that the explanation includes one design choice, using BCEWithLogitsLoss instead of a sigmoid layer plus BCELoss. Mentioning one such choice per block of code shows you made decisions, not just copied a template.
SECTION 02Why fit and transform are separate: a custom transformer
In scikit-learn code, the line-level question I ask most is about fit versus transform. A candidate who writes scaler.fit_transform(X_test) has a leakage bug: the scaler learns the test set's mean and standard deviation, so the test data is no longer truly unseen. Many candidates know this rule but cannot explain why the API is designed with two separate methods.
Writing a small custom transformer and explaining it aloud is the best way to make the idea stick. fit learns something from training data and stores it on the object. transform applies what was learned, without learning anything new. That separation is exactly what lets a Pipeline avoid leakage during cross-validation.
| Code line | What it learns | Where it should run | Why |
|---|---|---|---|
| fit(X_train) | Statistics such as medians or categories | Training data only | Learned values must not include test data |
| transform(X_test) | Nothing, it only applies stored values | Any data, including test and live data | Test data must be treated like new data |
| fit_transform(X_train) | Same as fit, then applies it | Training data only | A shortcut, not a different operation |
| Pipeline.fit inside cross_val_score | Refits every step on each training fold | Automatic | Prevents leakage across folds |
import numpy as np
import pandas as pd
from sklearn.base import BaseEstimator, TransformerMixin
class QuantileCapper(BaseEstimator, TransformerMixin):
"""Cap each numeric column at quantiles learned in fit."""
def __init__(self, lower=0.01, upper=0.99):
self.lower = lower
self.upper = upper
def fit(self, X, y=None):
X = pd.DataFrame(X)
self.low_ = X.quantile(self.lower)
self.high_ = X.quantile(self.upper)
return self
def transform(self, X):
X = pd.DataFrame(X).copy()
return X.clip(lower=self.low_, upper=self.high_, axis=1)
train = pd.DataFrame({"amount": np.arange(1, 101)})
test = pd.DataFrame({"amount": [-50, 50, 10_000]})
cap = QuantileCapper().fit(train)
print(cap.transform(test)["amount"].tolist())"fit learns the 1st and 99th percentile of each column from
the training data and stores them with a trailing underscore,
which is the scikit-learn convention for learned attributes.
transform only applies those stored limits. It copies the
input first, so the caller's dataframe is not modified.
On the test set, an extreme value of 10,000 gets capped at
the training 99th percentile. If I had called fit on the test
set, the cap would move to include that outlier, which is
leakage, and live data could be treated differently from
test data.
Because it follows the BaseEstimator API, I can put it in a
Pipeline and it refits correctly on every fold."
Running the capper shows the values clipped to the training range: the negative value rises to the lower cap and the large one drops to the upper cap. Being able to predict that output before you run it is a good test of your own understanding.
SECTION 03Talking through a code review of your own ML code
Some interviews include a code review round: the interviewer shares a snippet, sometimes your own, and asks what you would change. This tests whether you can explain code critically, not just defend it. The best approach is to review in a fixed order: correctness first, then data leakage and reproducibility, then readability and performance.
Practising this aloud on your own code is uncomfortable but extremely useful. Pick a script you wrote months ago and review it as if a colleague had written it. You will almost certainly find something to fix, and that becomes a great interview story.
| Review order | What to look for in ML code | Example finding | How to phrase it |
|---|---|---|---|
| 1. Correctness | Wrong shapes, wrong labels, off-by-one splits | Labels shifted by one row after a merge | This merge can misalign labels when IDs repeat |
| 2. Leakage | Fitting on full data, future features | Encoder fitted before the split | This fit sees test data; move it into the pipeline |
| 3. Reproducibility | Missing seeds, unpinned versions, local paths | No random_state in the split | Results will change each run; set a seed |
| 4. Readability and speed | Loops over rows, long functions, magic numbers | iterrows over a large dataframe | A vectorised operation does this much faster |
import pandas as pd
from sklearn.preprocessing import OneHotEncoder
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
df = pd.read_csv("C:/Users/me/Desktop/data.csv")
enc = OneHotEncoder()
X = enc.fit_transform(df[["city", "plan"]])
y = df["churned"]
X_tr, X_te, y_tr, y_te = train_test_split(X, y)
model = LogisticRegression().fit(X_tr, y_tr)
scores = []
for i, row in df.iterrows():
scores.append(row["usage"] * 0.37)
print(model.score(X_te, y_te))from pathlib import Path
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
DATA = Path("data") / "customers.csv" # relative path
USAGE_WEIGHT = 0.37 # named, not magic
df = pd.read_csv(DATA)
X, y = df[["city", "plan"]], df["churned"]
X_tr, X_te, y_tr, y_te = train_test_split(
X, y, stratify=y, random_state=42) # reproducible
model = Pipeline([
("enc", ColumnTransformer([
("cat", OneHotEncoder(handle_unknown="ignore"), ["city", "plan"])
])),
("clf", LogisticRegression(max_iter=1000)),
])
model.fit(X_tr, y_tr) # encoder fit on train only
scores = df["usage"] * USAGE_WEIGHT # vectorised
print(model.score(X_te, y_te))
Talk through the changes in review order: the encoder was fitted before the split, so I moved it into a Pipeline; handle_unknown="ignore" stops new cities from crashing prediction; the split had no seed; the path only worked on one laptop; and the row loop became one vectorised line.
Ordering your review this way shows priorities. Fixing a magic number first while missing the leakage would suggest you look at style before substance.
SECTION 04Drills for explaining ML code aloud
These drills build the habit of line-level explanation. Use your own code wherever possible. An Uncodemy trainer can also point at random lines in your project during a mock round, which is the closest thing to the real experience.
POINT-AND-EXPLAIN (15 minutes)
1. Open a training or preprocessing script you wrote.
2. Ask a friend to point at any line, or pick lines at random.
3. For each line, answer aloud at four levels:
- What does it do?
- Why is it here?
- What breaks if I delete it? Loud or silent?
- What else could I have written instead?
4. Mark any line where you said 'I'm not sure'.
5. Look those up, test them, and repeat tomorrow.DELETE-A-LINE (20 minutes)
Take the training loop above (or yours) and, one at a time:
[ ] Remove opt.zero_grad() -> what happens to the loss?
[ ] Remove model.eval() -> does val accuracy change?
[ ] Remove torch.no_grad() -> check memory and speed
[ ] Replace loss.item() with loss -> what type is total?
[ ] Use BCELoss without sigmoid -> what error or result?
Write one sentence for each result. These sentences are
your best interview answers, because you saw them happen.REVIEW MY OWN SCRIPT: ______________
1. Correctness issues found: _______________
2. Leakage risks found: ____________________
3. Reproducibility gaps: ___________________
4. Readability / speed fixes: ______________
The ONE change that mattered most: ________
How I would explain it in 30 seconds:
_______________________________________
SECTION 05Habits that make your ML code easy to explain
Code that is easy to explain is usually also easier to maintain. These habits help with both.
- Name things after their purpose — A variable called
train_dlorusage_weightexplains itself. Names likex2ortempforce you to explain them aloud. - Comment the why, not the what — Write comments like 'clear grads, PyTorch accumulates by default' instead of 'zero grad'. They become your interview lines.
- Keep functions short — A function that does one thing can be explained in one sentence. Long functions are hard to explain and harder to test.
- Break code on purpose — Delete or change a line and watch what happens. First-hand results are far easier to explain than rules you read.
- Know your library defaults — Know default values such as max_iter or handle_unknown for the tools you use. Interviewers often ask about them.
SECTION 06Interview Q&A — Explaining ML code
Q1What happens if you forget optimizer.zero_grad() in PyTorch?
PyTorch accumulates gradients by default, so each backward call adds to the previous gradients. Without zero_grad, every step uses a growing sum of past gradients, which makes updates too large or erratic. Training may still run, which is why the bug is easy to miss.
Q2Why do we call model.eval() before validation?
It switches layers like dropout and batch normalisation to evaluation behaviour. Dropout is turned off and batch norm uses running statistics instead of batch statistics. Without it, validation results are noisier and usually worse than the model's real performance.
Q3Why is fit_transform on the test set a problem?
Because fit learns statistics from the data it sees. Fitting on the test set means the transformer has learned from data that should be unseen, which is leakage. The test set should only be transformed using values learned from training data.
Q4Why use BCEWithLogitsLoss instead of a sigmoid layer and BCELoss?
BCEWithLogitsLoss combines the sigmoid and the loss in one numerically stable operation, which avoids problems with very large or very small logits. The model outputs raw logits, and I apply sigmoid only when I need probabilities for prediction.
Q5Review this code: an encoder is fitted on the full dataset before the split.
The main issue is leakage: the encoder learns categories from test rows. I would move it into a Pipeline with the model so it is fitted only on training data, and set handle_unknown to ignore so unseen categories in live data do not crash prediction. I would also check for a fixed random seed in the split.
SECTION 07Test yourself — Explaining ML code quiz
Five questions. No sign-up.
0 / 5Pick an answer to see why it is right or wrong.
SECTION 08Frequently asked questions
How do I explain my code in a machine learning interview?
For each important line, say what it does, why it is there, what breaks if it is removed and what alternative you considered. Focus on lines that prevent silent bugs, such as zero_grad, eval mode and fitting only on training data.
What PyTorch questions are asked in ML engineer interviews?
Common ones include why zero_grad is needed, the difference between train and eval mode, what torch.no_grad does, how loss functions like BCEWithLogitsLoss work, and how to write a basic training and validation loop.
What is the difference between fit and transform in scikit-learn?
fit learns parameters such as means, quantiles or categories from data and stores them. transform applies those stored parameters to data without learning anything new. fit_transform does both on the same data.
How do I prepare for a code review round as an ML engineer?
Practise reviewing your own old scripts in a fixed order: correctness, leakage, reproducibility, then readability and speed. Say what is good first, then the most important issue and how to fix it.
Should I write custom transformers in my ML projects?
When you need a preprocessing step that scikit-learn does not provide, yes. A small transformer that follows the fit and transform API can go inside a Pipeline, which keeps your preprocessing safe and reusable.
Is there a course that covers ML Engineer interview answers?
Yes. Uncodemy's Machine Learning Training Course in Noida (classroom and online) includes live projects, one-to-one mock interviews and resume reviews, so you practise ML Engineer interview answers with trainer feedback before the real interview. You can book a free demo class from this page to see how the sessions work.
SECTION 09Related reads
Classroom & online · Noida
Write ML code you can explain line by line
Our Machine Learning Training Course covers PyTorch, scikit-learn pipelines and clean ML code, with regular code walkthrough and review practice before interviews.
Talk to a counsellor for current fees- PyTorch and scikit-learn labs
- Code review practice
- Mock technical interviews
- Weekday & weekend batches