Version Control and Developer Tooling Questions
The everyday toolchain of software work: version control with Git (branching, merging, rebasing, conflict resolution, using git bisect to find a regression), command-line and shell proficiency for day-to-day navigation, log inspection, and troubleshooting, IDE and editor workflows, build systems and package/dependency management (npm, Maven, pip, Gradle, CocoaPods, and embedded/cross-compilation toolchains), and the growing practice of AI-assisted coding: using, reviewing, and verifying AI-generated code and tests. Deliberately generic across languages and stacks; language- and domain-specific frameworks live in their own categories. This topic covers a developer's individual command of these tools, not: writing durable shell automation and glue scripts (Shell Scripting and Automation owns that), producing, versioning, and publishing build artifacts or container images (Build Automation and Artifact Management owns that), release cadence and change governance (Release Management and Change Control owns that), or diagnosing a live production incident end to end (Performance Troubleshooting and Incident Response and the Observability topics own that).
An AI assistant writes model-serving code that loads serialized artifacts with pickle from disk. Why is this risky in production, how would you review the code, and what safer alternatives would you propose?
Sample Answer
Direct answer
Python's pickle module does not just store data, it stores instructions for reconstructing arbitrary Python objects, and reconstructing an object can involve calling arbitrary functions. Loading a pickle file is not "read some data," it is "run whatever code the file's author encoded into it." In a model-serving path, that turns a data-loading step into a remote code execution (RCE, an attacker's code running on your server as if it were your own) vector the moment the artifact could ever come from, or be tampered with in, a location you do not fully control.
Approach
The code under review looks innocent:
import pickle
def load_model(path: str):
with open(path, "rb") as f:
model = pickle.load(f) # executes arbitrary code embedded in the file
return model
Nothing here looks unusual, which is exactly the danger: a reviewer skimming for correctness bugs will not catch this, because the code is "correct." The risk is a property of the file format, not a bug in this function.
Why this actually executes code, demonstrated
This is illustrative but genuinely executed, not hand-written output: a class can define __reduce__ (the method pickle calls to figure out how to reconstruct an object) to return an arbitrary function and arguments instead of normal state, and unpickling calls that function.
import pickle, os
class Payload:
def __reduce__(self):
# __reduce__ tells pickle how to "reconstruct" this object; here,
# "reconstruction" means calling os.system with an arbitrary command.
return (os.system, ("echo PWNED-BY-UNPICKLING",))
data = pickle.dumps(Payload())
print("Serialized bytes look completely ordinary:", data[:40], "...", flush=True)
print("Now unpickling (this is the 'just loading a model file' step):", flush=True)
pickle.loads(data) # this is where the arbitrary command actually runs
Running this (Python 3, standard library only) prints:
Serialized bytes look completely ordinary: b'\x80\x05\x953\x00\x00\x00\x00\x00\x00\x00\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x18echo PWN' ...
Now unpickling (this is the 'just loading a model file' step):
PWNED-BY-UNPICKLING
The last line is the shell command actually executing, triggered purely by calling pickle.loads on bytes that look like ordinary binary data. A real attack would run something far less polite than echo.
How I would review the code
- Trace the full path: where is the artifact produced, where is it stored, and who or what can write to that location between training and serving?
- Confirm the artifact is versioned, and ideally signed and hash-checked, so serving code refuses to load a file that has been tampered with or does not match a known-good hash.
- Ask whether the serving process runs with least privilege (no unnecessary filesystem, network, or shell access), so that even if deserialization is exploited, the blast radius is smaller.
- Check whether the artifact ever crosses a trust boundary: was it produced entirely in-house on infrastructure you control, or could it originate from a less-trusted source, a shared bucket, an external contributor?
Safer alternatives
safetensors(a format designed specifically to store tensor weights without any code-execution risk) for model weights.- ONNX (Open Neural Network Exchange, a portable, framework-neutral inference format) when you need to serve a model without the code-execution surface of a framework-native pickle.
- Plain JSON or Parquet for non-tensor metadata and configuration.
- If custom Python objects genuinely need serializing, an explicit schema-based serializer (something that reconstructs a known, fixed set of fields rather than arbitrary objects) instead of pickle.
joblib.loadis sometimes suggested as a "safer" alternative; it is not, for this exact risk, since joblib uses pickle under the hood for arbitrary Python objects and inherits the same arbitrary-code-execution surface. It only helps with load performance, not with this specific vulnerability.
Trade-offs and edge cases to check
Signed, hash-checked artifacts add operational overhead (a signing step, a registry, key management) that a small team may resist, but the alternative is trusting every file on the serving path implicitly. Edge cases worth testing: an artifact with a valid hash but from an unexpected source, a serving process that falls back to a cached or default artifact if the primary fails to validate (make sure that fallback path is not itself less trusted), and any code path where the artifact location is derived from user input rather than a fixed, controlled configuration value.
An AI-suggested TensorFlow model fails with a shape mismatch during training. How do you debug the issue step by step, and where do you stop trusting the AI suggestion and inspect the tensors yourself?
Sample Answer
Direct answer
I debug shape mismatches by printing the tensor shape at every stage of the pipeline and computing by hand what each shape should be, rather than guessing from the error text alone. I stop trusting the AI's suggested fix the moment it proposes a reshape, transpose, or flatten without explaining what each dimension represents, because a reshape can be numerically valid (the total element count matches) while being semantically wrong, silently swapping which axis is batch, time, or feature.
Step by step
- Print the shape after every preprocessing stage, from raw input through batching, so you know exactly where the numbers stop matching expectations.
- Compare the model's expected input shape (
model.summary()in Keras shows this per layer) against the actual batch shape being fed in. - Check the label shape separately from the input shape, since a common source of this exact bug is a mismatch between the label encoding and the loss function:
sparse_categorical_crossentropyexpects integer class labels shaped like(batch,), whilecategorical_crossentropyexpects one-hot labels shaped like(batch, num_classes). Mixing the wrong loss with the wrong label shape produces a shape-mismatch error that has nothing to do with the model architecture itself. - Isolate the first failing operation: a reshape, a concatenation of multiple branches, or the loss computation itself.
- Run a single small batch through the model manually and inspect shapes at each layer boundary before touching real data, since this makes the failure reproducible and cheap to iterate on.
Demonstrating the underlying arithmetic
TensorFlow is not installed in the environment I authored this answer in, so I am not claiming to have executed real TensorFlow here. What I can genuinely execute and show is the shape arithmetic itself, using NumPy, since the underlying reasoning (does the element count match the target shape) is identical to what TensorFlow checks:
import numpy as np
# Simulates an AI-suggested reshape step: a flattened batch of 32 grayscale
# 28x28 images (32 * 784 = 25,088 elements) being reshaped back to images,
# but incorrectly assuming 3 color channels instead of 1.
batch = np.zeros((32, 784), dtype="float32")
print("Flattened batch shape:", batch.shape, flush=True)
print("28*28*1 (grayscale) =", 28 * 28 * 1, flush=True)
print("28*28*3 (assumed RGB) =", 28 * 28 * 3, flush=True)
try:
images = batch.reshape((32, 28, 28, 3))
except ValueError as e:
print("Reshape failed:", e, flush=True)
Running this prints:
Flattened batch shape: (32, 784)
28*28*1 (grayscale) = 784
28*28*3 (assumed RGB) = 2352
Reshape failed: cannot reshape array of size 25088 into shape (32,28,28,3)
32 times 784 is 25,088 total elements, but 32 times 28 times 28 times 3 asks for 75,264, so the reshape is impossible. TensorFlow raises an analogous shape-incompatibility error at the equivalent point in a real pipeline (typically at model-build or fit time, naming the two incompatible shapes), and the exact wording depends on TensorFlow version and where in the graph the mismatch surfaces, so I would not quote a specific TF error string as gospel. The debugging discipline is the same either way: compute the expected size by hand, and find exactly where the actual and expected numbers diverge.
Where I stop trusting the AI suggestion
If the AI's fix is "just reshape to X" or "just add .squeeze() here" without explaining what each axis represents before and after, I stop and inspect the tensors myself. A reshape from (32, 28, 28, 1) to (32, 784) is safe because it is flattening the same logical data; a reshape from (32, 784) to (32, 28, 28, 3), as above, is not just wrong, it is wrong in a way that would only be caught by someone checking the arithmetic, since the AI's suggestion can be syntactically valid Python that happens to compute a number that does not match.
Trade-offs and pitfalls
Fixing a shape error by inserting a reshape that "makes the numbers work" without understanding the semantics is the single most common way this class of bug reappears later in a different form, for example silently training on transposed batch and feature axes rather than crashing outright. A crash on a shape mismatch is the friendlier failure mode; a reshape that quietly succeeds with the wrong semantics is the dangerous one, because training proceeds and the model just learns something wrong.
You asked two AI tools to implement the same model evaluation utility. One solution is shorter but uses dense nested loops; the other is longer with clearer abstractions and tests. How would you decide which one to adopt?
Sample Answer
Direct answer
I would decide based on correctness, maintainability, and actual algorithmic cost for realistic data sizes, not line count. Shorter is not automatically better if it is harder to verify, and longer is not automatically better if the extra structure does not earn its keep; the deciding factors are whether each version is provably correct, how expensive it will be to change later, and whether its performance profile actually matches how the utility will be used.
Structured elaboration
Correctness first. Read both implementations against the spec and check edge cases: empty input, a single example, ties in whatever the utility is comparing. Run both on the same representative inputs and confirm they agree; if they disagree, that disagreement itself is the first thing to resolve, since one of the two is simply wrong.
Readability and future maintenance. A model evaluation utility tends to be touched again, new metrics added, edge cases discovered later, so the version with clearer abstractions usually wins if this code is going to live in the codebase rather than being thrown away after one use.
Algorithmic cost, reasoned about rather than measured. Dense nested loops over the evaluation set typically mean quadratic behavior, roughly proportional to the square of the input size, if the loops are comparing every example against every other example, versus a vectorized or hash-based approach that can often do the same comparison in roughly linear time. Whether that difference matters depends entirely on how large the real evaluation sets are: for a few hundred examples the two approaches may be indistinguishable in practice, while for a dataset with hundreds of thousands of rows a quadratic implementation can become the slowest part of an otherwise fast pipeline. I would reason about this from the algorithm's structure rather than trust a specific benchmark number from either AI tool, since a wall-clock timing claim is environment-dependent and not something I would treat as verified without running it myself on the actual target data size.
Test coverage. I trust the version with explicit tests more by default, especially for evaluation code, since a silent bug in a metric calculation is expensive: it can make a genuinely worse model look better, or vice versa, and nobody notices until much later.
Worked example
Say the utility computes pairwise similarity between predicted and true label sets for a batch of examples. The short version:
def matches(preds, labels):
count = 0
for p in preds:
for l in labels:
if p == l:
count += 1
return count
This nests one loop inside another, so its cost grows with the product of the two input sizes: for a batch of size n compared against itself, that is roughly n-squared comparisons. The longer version might instead build a set from labels once and then check membership per prediction, which drops the cost to roughly linear in the size of preds for the lookup, at the cost of readability going from "obviously what it says" (two nested loops, immediately understandable) to needing the reader to know sets give near-constant-time membership checks. For a small evaluation set (a few hundred examples), the difference is unlikely to be the bottleneck anywhere in the pipeline; for a large one, the nested-loop version is the one worth rewriting.
How I would actually decide
- Read both implementations against the spec.
- Run them on the same representative inputs and confirm they produce identical results.
- Reason about how each will scale to the evaluation sizes this will realistically see in production, and only benchmark directly on realistic data if that reasoning leaves genuine doubt.
- Prefer the version that is easier to extend, review, and verify, unless profiling on real data shows the other one is a genuine bottleneck.
Trade-offs and pitfalls
Optimizing for maintainability by default is the right instinct until it isn't: on a hot path processing millions of evaluation rows, the "obviously correct" nested-loop version can become a real cost, so this is a decision to revisit if the data volume changes, not a one-time choice. Conversely, adopting the longer, more abstract version purely because it looks more sophisticated, without verifying it is actually correct or actually needed, trades simplicity for complexity without buying anything.
An AI assistant produces a training script for an imbalanced classification problem using accuracy as the main metric and a random train-test split. Review the approach: what problems do you see, and how would you correct them?
Sample Answer
Direct answer
Three problems stand out: accuracy is the wrong headline metric for imbalanced data because it rewards a model for ignoring the minority class, a plain random split can break temporal or grouped structure in the data and leak information between train and test, and there is no threshold-aware evaluation at all. I would fix this with a stratified split (or a time-based split if the data has a time axis), report precision, recall, F1, and PR AUC (precision-recall area under the curve, a metric that summarizes performance across thresholds and is more informative than accuracy when positives are rare) instead of accuracy, and keep any resampling or class weighting strictly inside the training fold, using a pipeline so the split stays honest end to end.
Structured elaboration
Why accuracy is misleading here. Consider a dataset of 1,000 examples where 950 are negative and only 50 are positive (a 5% positive rate). A model that predicts "negative" for every single example never learns anything about the positive class, yet: true positives = 0, false positives = 0, false negatives = 50, true negatives = 950. Accuracy = 950/1000 = 0.95, a 95% score, while recall = 0/50 = 0%, meaning it catches literally none of the cases you actually care about. Precision is undefined (0 predicted positives). A 95%-accurate, completely useless model is the exact failure mode accuracy hides.
Why a random split is risky. If the data has any temporal or grouped structure (transactions over time, multiple rows per customer), a random split can put future information into the training set or split a customer's rows across both train and test, both of which inflate the reported score relative to how the model will actually perform in production. The fix is a stratified split when class balance is the only concern, and a time-based split (train on earlier data, test on later data) whenever the data has a genuine time axis, since stratification alone does not address temporal leakage.
Correcting the training script. Report precision, recall, F1, and PR AUC instead of, not in addition to, treating accuracy as the headline number. Keep any preprocessing or class-imbalance handling (resampling, class weights) inside a pipeline that is fit only on the training fold, so cross-validation numbers stay honest rather than leaking information from validation data into preprocessing decisions.
Worked example
A second worked example on the loss side. Suppose the same AI assistant later suggests softmax activation with categorical cross-entropy for a task where each example can carry more than one label at once, for instance tagging a support ticket with several relevant categories simultaneously (billing and account-access at the same time). Softmax forces the predicted probabilities across classes to sum to 1, which assumes exactly one correct class per example, so it is the wrong choice for a genuinely multi-label problem. The correct setup is a sigmoid activation on each output unit (an independent probability per class) with binary cross-entropy (BCE, a loss function that scores each class's yes/no prediction independently) summed or averaged across labels, and metrics reported per label rather than as a single top-1 accuracy.
Checking AI-generated feature SQL for the same imbalance-distorting leakage. If the features for this dataset come from AI-generated SQL, two specific bugs are worth checking by hand: a window function whose boundary silently includes future rows (ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING instead of ... AND CURRENT ROW, which lets a rolling average see data from after the prediction timestamp), and a join that fans out one training row into several duplicates because the joined table is not unique per key, which distorts the apparent class balance, especially for a rare positive class.
Verifying a claimed win rather than accepting it. If a teammate or an AI assistant reports "switched the resampling technique, F1 improved," that single number is not enough to accept the change. I would check precision and recall separately (F1 can rise even while recall quietly collapses, if precision spikes enough to compensate) and check calibration (whether the model's predicted probabilities still mean what they claim, for example by comparing predicted-probability buckets against the observed positive rate within each bucket) before accepting that the change is actually an improvement rather than a different, worse trade-off wearing a better-looking single number.
Trade-offs and pitfalls
Stratification fixes class ratio but does nothing for temporal leakage, so time-ordered data needs both considerations together, not one instead of the other. Aggressive resampling can improve recall while quietly hurting calibration, so a "better" F1 is not automatically a better model for a use case where the actual predicted probability matters, not just the class label.
An AI assistant hands you distributed PyTorch training code using DDP. Walk through what you'd specifically check before trusting it: what subtle bugs or performance issues tend to hide in AI-generated distributed-training code around process setup, synchronization, dataloading, and metric aggregation.
Sample Answer
Direct answer
I review DDP (Distributed Data Parallel, PyTorch's mechanism for training the same model across multiple processes, typically one per GPU, keeping their gradients synchronized) code by walking the four places distributed bugs actually hide: process setup, synchronization inside the training loop, dataloading, and metric aggregation across ranks (a rank is one participating process in the distributed job). PyTorch is not installed in the environment I am writing this answer in, so the code below is presented for review, illustrating what I would check, not as a claimed execution trace.
Approach and code under review
def main(rank, world_size):
dist.init_process_group(backend="nccl", rank=rank, world_size=world_size)
torch.cuda.set_device(rank)
model = MyModel().to(rank)
model = DistributedDataParallel(model, device_ids=[rank])
sampler = DistributedSampler(dataset, num_replicas=world_size, rank=rank)
loader = DataLoader(dataset, sampler=sampler, batch_size=batch_size)
for epoch in range(num_epochs):
for batch in loader:
inputs, labels = batch
optimizer.zero_grad()
outputs = model(inputs.to(rank))
loss = criterion(outputs, labels.to(rank))
loss.backward()
optimizer.step()
acc = compute_accuracy(model, val_loader)
print(f"Epoch {epoch}: accuracy = {acc}")
Key points to check
Process setup
- Confirm
init_process_groupuses the right backend for the hardware (ncclfor multi-GPU is standard, not, say,gloo, which is meant for CPU or as a fallback), and that rank, world size, and a timeout are all actually wired correctly, since a missing or default timeout can leave a hung job waiting silently instead of failing loudly. - Confirm every process sets a distinct GPU via
torch.cuda.set_device(rank)before any tensors move to that device; a common AI-generated mistake is calling this after some setup already ran on the default device. - Seeds should be set per worker, but in a way that stays reproducible across runs, not literally identical across ranks if that would make every rank do the exact same random augmentation.
Synchronization inside the loop
- The model should be moved to its device and wrapped in
DistributedDataParallelexactly once, in that order (move to device, then wrap), not wrapped before the device move. - The code above is missing
sampler.set_epoch(epoch)inside the epoch loop. Without it,DistributedSamplerreuses the same shuffling order every epoch across all ranks, which quietly weakens training (the model sees the same partitioning of data in the same order every epoch instead of a fresh shuffle) without producing any error. - Watch for hidden synchronization points: calling
.item()on a GPU tensor inside the training loop, or an unnecessaryall_reduce, both force a synchronization across all ranks and can silently degrade throughput without any visible bug.
Dataloading
DistributedSampleris present here, which is correct: without it, every rank would iterate over the entire dataset independently, each seeing the exact same duplicated data.- Check whether
drop_lastis set intentionally; if the dataset size is not evenly divisible byworld_size, the last batch can differ in size across ranks, which can break assumptions elsewhere (like a fixed batch size used in metric aggregation). - Worker count and
pin_memorysettings affect throughput but do not affect correctness; they matter for performance review, not for trust.
Metric aggregation across ranks
- The code above computes
acc = compute_accuracy(model, val_loader)and prints it directly on every rank. This is a real bug: ifcompute_accuracyruns independently per rank on a differently-partitioned slice of validation data, each rank prints a different, locally-computed accuracy, none of which is the actual overall accuracy, and naively averaging those per-rank accuracies afterward is still wrong if the ranks do not have equal numbers of examples, since it is equivalent to weighting each rank's average equally rather than weighting by example count. - The fix: aggregate raw counts (correct predictions and total predictions) across ranks with
all_reducefirst, then compute the ratio once from the summed counts, and have only rank 0 print or log the final result, so the same number does not get printed once per GPU. - A concrete trace of why naive averaging is wrong: say rank 0 validates on 90 examples and gets 81 correct (0.9 accuracy), and rank 1 validates on only 10 examples and gets 5 correct (0.5 accuracy), an uneven split that happens naturally whenever the validation set doesn't divide evenly across ranks. Naively averaging the two per-rank accuracies gives (0.9 + 0.5) / 2 = 0.70. Aggregating the raw counts first gives (81 + 5) / (90 + 10) = 86 / 100 = 0.86, the actual overall accuracy. The two ranks don't have equal numbers of examples, so the naive average silently understates accuracy by 16 points here; that gap only disappears by coincidence, if every rank happens to validate on exactly the same number of examples.
- Validation should run under
model.eval()andtorch.no_grad(); the excerpt above does not show this, which is worth flagging explicitly rather than assuming it happens insidecompute_accuracy.
Edge cases and performance considerations
- Multi-node training (not just multi-GPU on one machine) needs the rendezvous (the process by which the distributed workers discover each other's network addresses and confirm everyone is present before training starts) and network configuration checked separately, since a setup that works on a single node with
ncclcan fail silently or hang across nodes if the network backend or address configuration is wrong. - Excessive communication overhead from unnecessary synchronization points, or from a batch size per GPU that is too small relative to the communication cost of each step, is a performance issue rather than a correctness one, but it is exactly the kind of thing that reads as "training is just slow" rather than surfacing as an obvious bug.
- Checkpoint saving on every rank instead of only rank 0 both wastes I/O and risks a race condition if multiple ranks write to the same path simultaneously.
The same failure category shows up beyond DDP
This exact pattern, code that quietly assumes a single-process execution model and breaks only once you actually distribute the work, shows up outside PyTorch too. An AI-proposed feature pipeline written in pandas for a dataset that will later run on Spark at 100 times the scale has the same shape of risk: pandas operations assume the whole dataset fits in memory on one machine and execute eagerly in a fixed row order, while Spark distributes data across partitions and executes lazily, so logic that depends on row order, a global sort, or an operation that silently materializes the entire dataset in memory can be correct in the small pandas prototype and either wrong or prohibitively expensive once it actually runs distributed. The check is the same in spirit as the DDP review above: read the code specifically for assumptions about running in one place, in one order, with everything visible to a single process, since that is exactly the assumption distributed execution breaks.
Trade-offs and pitfalls
None of the bugs above (missing set_epoch, per-rank metric printing, unnecessary synchronization) throw an error. Training runs to completion, a number gets printed, and everything looks fine unless you specifically know what correct DDP output should look like. That is exactly why a checklist matters more here than in single-process code: the failure signal is silence, not a crash.
Unlock Full Question Bank
Get access to all 8 Version Control and Developer Tooling interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.