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).
You ask an AI assistant to modify a function in an existing codebase, but it quietly changes the function's signature and breaks several callers elsewhere in the code. What safeguards would you put in place to catch this before merging, and how would you preserve interfaces and backward compatibility when accepting AI-generated changes going forward?
Sample Answer
Direct answer
I would put safeguards in place before the change ever lands in the main branch, not rely on catching it in review by eye: an explicit "do not change the signature" constraint in the prompt, a search across the codebase for every caller of the function being touched, and a CI (continuous integration) check that actually exercises those callers. Catching a broken signature by carefully reading a diff does not scale, since the break is often invisible in the diff of the function itself, it only shows up as a failure somewhere else entirely.
Structured elaboration
Preventing it at the prompt level
- Constrain the request explicitly: "modify the internal logic but do not change the function's name, parameter order, parameter names, or return type," rather than a general "improve this function" that leaves the interface up for grabs.
- Provide the function's current callers as context, so the assistant can see what would break, rather than reasoning about the function in isolation.
Catching it before merge
- Search the codebase for every call site of the changed function before approving the change (a simple text or symbol search, or an IDE's find-references), and confirm each one still matches the new signature.
- Run the full test suite, including integration tests that exercise the function through its real callers, not only unit tests that call the function directly with hand-picked arguments a test author already expects to work.
- Add a lightweight contract check where the interface is easy to characterize: a type-checked function signature, or a small test that specifically asserts the function's signature has not changed, so a future AI-assisted change trips this test immediately rather than being caught downstream.
Preserving backward compatibility going forward
- Default to additive changes: add a new optional parameter with a default rather than changing an existing required one, so old call sites keep working unmodified.
- When a breaking change is genuinely necessary, use a wrapper or adapter function that preserves the old signature and forwards to the new one, and version the interface explicitly if it is public.
- Require an explicit compatibility note in any pull request that touches a function with more than a couple of callers: what changed, who is affected, and how it was validated, so a reviewer does not have to reconstruct that from the diff alone.
Preserving behavior after a pure refactor
A pure refactor (same signature, intended to be behavior-preserving, done purely for readability) has its own distinct checklist beyond signature preservation: confirm the output is identical on a representative set of real inputs, not just that the code compiles; check latency has not regressed, since a "cleaner" rewrite can accidentally introduce an expensive operation inside a loop that used to be outside it; and check error handling specifically, since a refactor is a common place for an exception type or an edge-case branch to silently disappear during a rewrite that otherwise looks like a pure simplification.
Worked example
An AI assistant is asked to "clean up" a function parse_config(path) used in six places across the codebase. It returns a cleaner implementation, but the new version returns None on a missing file instead of raising a FileNotFoundError the way the original did, a behavior change with no signature change at all, so a caller relying on the exception (a try/except FileNotFoundError block elsewhere) now gets a silent None instead, and fails later in a way that is much harder to trace back to this change. A signature-only check would miss this entirely, which is exactly why behavior and error-handling checks matter as a separate step from an interface check, and why "did the tests pass" is not sufficient if none of the six callers had a test covering the missing-file case.
Trade-offs and pitfalls
Contract tests and full-codebase caller searches add real time to every review, which can feel excessive for a function with one caller. The safeguards are worth scaling to the function's actual blast radius: a private helper used in one place needs a lighter check than a function called from a dozen places across the codebase, and treating every change with maximum ceremony trains people to skip the ceremony rather than actually protecting the changes that matter most.
How would you test AI-generated training or inference code for nondeterminism, hidden data leakage, and silent failure modes that may only appear after deployment?
Sample Answer
Direct answer
I would test this at three levels: unit tests that check determinism and leakage directly, integration tests that mimic realistic production data shapes, and production monitoring that catches the failure modes no offline test can fully replicate. The reason a dedicated pass is needed for AI-generated code specifically is that it can be syntactically correct and pass a naive test suite while still being statistically wrong, and that gap does not show up as a crash.
Structured elaboration
Nondeterminism. Fix every source of randomness explicitly: Python's own random module, NumPy, the ML framework's RNG (random number generator), and any data-loader shuffling. A useful concrete check is running the same job twice with the same seed and asserting the outputs match exactly, and once with a different seed to confirm you would actually notice if determinism silently broke:
import random
def run_trial(seed):
random.seed(seed)
# stand-in for "train a tiny model and return a summary statistic"
weights = [random.random() for _ in range(5)]
return sum(weights)
trial_a = run_trial(seed=42)
trial_b = run_trial(seed=42)
trial_c = run_trial(seed=7)
print("Trial A (seed=42):", trial_a, flush=True)
print("Trial B (seed=42):", trial_b, flush=True)
print("Trial C (seed=7): ", trial_c, flush=True)
print("A == B (same seed reproduces exactly):", trial_a == trial_b, flush=True)
print("A == C (different seed, expected to differ):", trial_a == trial_c, flush=True)
Running this prints:
Trial A (seed=42): 1.8991488243625052
Trial B (seed=42): 1.8991488243625052
Trial C (seed=7): 1.73393470277175
A == B (same seed reproduces exactly): True
A == C (different seed, expected to differ): False
That is the pattern to assert in an actual test: same seed produces bit-identical output, different seed does not. The stand-in computation here is deliberately simple; the same assertion pattern applies whether the thing being reproduced is five random numbers or a full training run.
Data leakage. Validate that train, validation, and test splits are separated by entity and time, not only by random row assignment, since a customer or a time window appearing on both sides of a split is a leak a purely random-assignment check would never catch. Add lineage checks so any label-derived or post-event column is flagged if it appears among the model's inputs, and add schema tests asserting that columns which should not exist at inference time are absent from the inference-time feature set.
Silent failures. Add canary tests using inputs shaped like real production edge cases: a batch that is entirely missing values in one column, out-of-range categorical values the model never saw during training, and degenerate inputs (all zeros, a single row). Compare the model's output distribution against a known baseline and set drift thresholds that would trigger an alert if predictions start looking meaningfully different from what training-time evaluation predicted. Log model version, a hash of the feature set, and confidence statistics on every request, so a silent failure in production leaves a trail to investigate rather than nothing at all.
After deployment. Monitor latency, error rate, prediction distribution skew, and the actual downstream business metric the model is supposed to move. Build an explicit rollback trigger tied to a sudden, sustained drop in one of these signals, rather than relying on someone noticing manually.
Worked example
A concrete silent-failure scenario: an AI-generated inference function silently coerces an unrecognized categorical value to a default rather than raising an error. Offline, this never triggers, because the test data only contains categories seen during training. In production, three months later, a new product category is introduced upstream, and every prediction for that category silently uses the default encoding instead of a meaningful one, degrading accuracy for that slice with no error, no log line, and no obvious signal until someone notices the business metric drifting for that segment specifically. A canary test with an explicitly unseen category, asserted to either raise or flag rather than silently default, would have caught this before deployment.
Trade-offs and pitfalls
A test suite this thorough takes real engineering time to build and maintain, and it is tempting to skip the entity/time-aware leakage checks in favor of the faster, purely random-split tests that are easier to write. The trade-off is that the easier tests give false confidence precisely on the failure modes that matter most, since a model can pass every naive test and still be wrong in a way that only shows up as a slow, hard-to-diagnose accuracy decay after deployment.
Design a reproducible AI-assisted coding workflow where prompts, generated snippets, human edits, tests, and approvals are all traceable in version control. What artifacts would you store, and how would you review them during code review?
Sample Answer
Direct answer
I would treat AI-assisted changes like an auditable build pipeline rather than just a diff: version the prompt, the model's raw output, the human's edits on top of it, the tests, and the approval as linked artifacts, not only the final merged code. The goal is that months later someone can reconstruct not just what the code does, but why it ended up written this way.
Structured elaboration
Artifacts to store
- The prompt text, model name and version, and any relevant parameters (temperature, system instructions), with a timestamp.
- The raw generated snippet or diff, captured before any human edits, as its own artifact.
- The human-edited version on top of that, so the two diffs can be compared: what did the model produce, and what did a person change.
- The test plan and actual test results for the change.
- Approval metadata: reviewer identity, timestamp, and a short reason for sign-off.
Where this lives
- A pull request template with explicit sections for the prompt, the raw generated output, and the human changes on top, so this is a normal part of every AI-assisted PR rather than a special process.
- A lightweight audit folder (for example
ai-audit/) or PR-attached transcript for the raw prompt and output, since pasting a long prompt directly into a commit message is unwieldy. - A commit trailer convention, for example
AI-Assisted-By: <model-name>, mirroring how Git already supports aCo-authored-by:trailer, sogit log --grepcan find every AI-assisted change later without needing external tooling.
Code review approach
- Review the raw AI diff and the human's edits on top of it separately: what did the model get right, what did the human have to fix, and does that pattern reveal something worth flagging (a class of mistake this model or this prompt style keeps making)?
- Confirm the tests actually exercise the AI-introduced logic and its edge cases, not just that line coverage went up. Coverage without meaningful assertions is a false sense of safety.
- Treat a missing prompt or missing raw-output artifact as a reason to request changes, the same way a PR with no description would be, because it breaks the traceability the whole workflow exists for.
Notebook and script-specific handling. Notebooks do not diff cleanly, since the underlying file format is JSON that embeds cell outputs and execution counts alongside code, so a tiny logic change can produce a huge, noisy diff. I would either (a) require a pre-commit hook that strips outputs before commit (a tool like nbstripout) so the diff that lands in review is just the code, or (b) treat the notebook purely as scratch space and require the reproducible logic to land in a plain .py module before merge. Either way, the prompt log for a notebook exploration session goes into the same audit artifact as script-based work, not left as an ephemeral comment in a cell that gets overwritten the next time someone re-runs it.
Hyperparameter-tuning-specific handling. Reproducibility for a tuning run needs more than the workflow above: every source of randomness (data split, model initialization, the tuner's own sampling) needs a fixed, recorded seed; every trial's configuration and resulting metric needs to be logged to an experiment tracker (for example MLflow or Weights and Biases), not just the winning configuration, since "why didn't we pick option B" is a real question later; and the environment itself needs pinning (a dependency lockfile or a container image digest), because a library version bump can silently shift results even with every seed fixed.
Worked example
A team asks an AI assistant to draft a nightly batch inference job (a scheduled process that scores a whole day's accumulated data at once, rather than one prediction at a time) that scores yesterday's transactions for fraud risk. The prompt, model, and generated draft are logged in the PR's audit section. A human engineer adds input validation and a --dry-run flag the AI draft omitted; that diff is visible separately from the AI's original output. Tests cover the empty-input case and a schema-mismatch case (a column the model expects is missing). Before deploy, a reviewer approves after comparing yesterday's predictions from a shadow run of the new job (running the new job alongside the real one on the same data, without letting its output affect anything, purely to compare results) against the existing production job's predictions on the same data, checking they agree closely enough to trust. A rollback step is wired in before deploy: the last several model artifacts are retained, and reverting to the prior artifact and config is a single documented action, so if next-day monitoring shows a skew in prediction distribution, the team can revert immediately rather than debugging under pressure.
Trade-offs and pitfalls
Documenting every AI interaction in full has real overhead, so the rigor should scale with blast radius: a one-line lint fix does not need a full prompt audit trail, but a change to a training pipeline or a production-serving path does. A prompt log alone proves traceability, not correctness, so it never substitutes for tests. And prompts that include pasted context can themselves contain sensitive data (customer records, internal credentials), so the audit artifacts need the same access control as the data they might reference, not looser controls just because they are "documentation."
An AI assistant generates a PyTorch training loop for you. Before you trust it and run it on real data, what would you check line by line, and what are the classic mistakes AI-generated training loops tend to make that are easy to miss on a quick read?
Sample Answer
Direct answer
I would read the loop line by line checking five specific things: device placement, gradient zeroing, the forward pass, the loss computation, and the backward/step sequence, since these are exactly where a quick read tends to skim past a subtly wrong line that still runs without error. PyTorch is not installed in the environment I am writing this answer in, so the training-loop excerpt below is illustrative for review purposes, not a claimed execution trace; I am not presenting fabricated output for it.
What I check, line by line
for epoch in range(num_epochs):
model.train()
for batch in dataloader:
inputs, labels = batch
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
- Device placement: are the model and every batch tensor moved to the same device before the forward pass? A model on GPU receiving CPU tensors (or vice versa) throws an error, but a subtler version, part of the pipeline (like a mask or an auxiliary tensor built later in the loop) never getting
.to(device), can slip past a quick read. - Gradient zeroing: does
optimizer.zero_grad()run at the start of every iteration, beforeloss.backward()? If it is missing or misplaced, gradients accumulate silently across batches instead of being computed fresh each time, and training still runs, it just learns something subtly wrong. - Forward pass: are inputs, labels, and any masks shaped the way the model expects, and is there an accidental
.cpu()call anywhere that would silently move a tensor off the GPU mid-pipeline? - Loss computation: is the loss a scalar, does it use the label format the loss function actually expects, and is any reduction (mean vs sum across the batch) intentional rather than a framework default nobody checked?
- Backward and step order: does
loss.backward()happen beforeoptimizer.step(), and is nothing calling.detach()on a tensor that still needs its gradient?
I also check for model.train() at the start of training and model.eval() plus torch.no_grad() (a context manager that disables gradient tracking, since validation does not need it and skipping it wastes memory and time) during validation, since AI-generated loops sometimes omit the mode switch entirely, which silently changes the behavior of layers like dropout and batch normalization between training and evaluation.
Classic mistakes that are easy to miss on a quick read
- Missing or misplaced
optimizer.zero_grad(), so gradients silently accumulate across batches instead of resetting each step. - No
model.eval()/model.train()switching, so dropout and batch normalization behave inconsistently between training and validation without any error being raised. - Validation code missing
torch.no_grad(), which still produces correct numbers but wastes memory and time computing gradients nobody uses. - A loss function and label shape that do not actually match (for example a multi-class setup mixing one-hot labels with a loss function that expects integer class indices), which either errors or, worse, silently computes something other than the intended loss.
Seeing the zero_grad effect concretely
PyTorch itself isn't available in this environment, but the accumulation bug doesn't actually depend on autograd internals, only on whether a number gets reset to zero between steps or not, so the same arithmetic is easy to show directly in plain Python and actually run:
def local_grad(w, target):
# dL/dw for L = (w - target) ** 2 -> 2 * (w - target)
return 2 * (w - target)
w = 0.5
targets = [1.0, 1.0, 1.0] # same target each "batch" on purpose, to isolate
# the accumulation effect from any real data change
print("With optimizer.zero_grad() called every step (correct):")
for step, y in enumerate(targets, start=1):
grad = 0.0 # zero_grad() resets the accumulator to 0 here
grad += local_grad(w, y)
print(f" step {step}: grad = {grad}")
print("With optimizer.zero_grad() missing (bug):")
grad = 0.0 # only initialized once, before the loop, never reset inside it
for step, y in enumerate(targets, start=1):
grad += local_grad(w, y) # this is what loss.backward() does to .grad
print(f" step {step}: grad = {grad}")
This was run exactly as shown and prints:
With optimizer.zero_grad() called every step (correct):
step 1: grad = -1.0
step 2: grad = -1.0
step 3: grad = -1.0
With optimizer.zero_grad() missing (bug):
step 1: grad = -1.0
step 2: grad = -2.0
step 3: grad = -3.0
w and the target are held fixed across all three steps here purely so the only thing changing is whether the accumulator resets, in a real loop w also updates every step, but the mechanism is the same one PyTorch's .grad accumulation follows: without a reset, .backward() keeps adding to whatever .grad already held, so by step 3 the optimizer would be reacting to a gradient three times too large, even though nothing about the data changed. That's the concrete shape of "training still runs, it just learns something subtly wrong."
Before running on real data
I run a single small batch through the loop first and watch whether loss decreases at all over a handful of steps on that tiny batch. That is a cheap, fast signal that the optimization path is wired correctly (gradients are flowing, the loss is actually connected to the model's parameters) before spending real time and compute on the full dataset.
Trade-offs and pitfalls
None of these mistakes throw an error, which is exactly what makes them dangerous: the loop runs to completion, the loss number exists, and everything looks fine on the surface while the model quietly learns worse than it should, or learns something that will not generalize. A training run finishing without a crash is not evidence the loop is correct.
You need an AI assistant to generate a feature engineering function from product requirements and a data dictionary. How would you phrase the prompt to minimize ambiguity and prevent leakage or schema drift?
Sample Answer
Direct answer
I would make the prompt explicit about inputs, outputs, and forbidden behavior rather than describing the feature in prose and hoping the assistant infers the constraints. Ambiguous prompts tend to produce elegant-looking but unsafe feature logic, code that runs and looks reasonable but silently uses information that would not actually be available at prediction time (leakage), or assumes a column shape that will not hold once the upstream data changes (schema drift, meaning the input data's structure shifts over time in ways the function was never told to expect).
Structured elaboration
A well-specified prompt for this task includes:
- The exact business goal and feature definition taken directly from the product requirements, not paraphrased, so there is one source of truth for what the feature is supposed to represent.
- The full data dictionary: column names, types, allowed values, and timestamp semantics (when is each column actually populated relative to the event being predicted).
- An explicit role for every column: which are raw inputs, which are derived features, which are identifiers, and which are target-adjacent fields that must never be used as inputs.
- A stated leakage policy: no fields populated after the prediction point, no timestamps from the future relative to the prediction time, and no use of the label or anything derived from it.
- Schema-drift constraints: preserve a documented output column order and schema, and specify exactly how the function should behave on missing or unseen fields (fail loudly, default safely, or something in between), rather than leaving that undefined.
- The desired output shape: a pure function, a scikit-learn transformer (a reusable object implementing
fit/transformso the same logic runs identically in training and serving), or a plain function with accompanying tests, whichever matches how the feature will actually be consumed downstream. - An explicit instruction to state assumptions, so the assistant lists what it is inferring rather than silently inventing a new derived column that was never asked for.
Scikit-learn pipeline schema alignment
When the target output is specifically a scikit-learn Pipeline (a container that chains several preprocessing/model steps so they run in a fixed sequence) or ColumnTransformer (a step that applies different preprocessing to different columns, then combines the results) step, rather than a standalone function, the prompt needs one more layer of specificity: name exactly which columns the transformer will see at fit time (when it learns parameters from the training data, like a mean or a vocabulary) versus transform time (when it applies those already-learned parameters to new data, without learning anything new), whether the transformer needs to handle a column that is present in training but absent at serving time, and whether the output feature names and order need to stay stable across pipeline versions, since a churn-prediction pipeline that silently reorders its output columns between training and serving is a schema-drift bug that will not show up as an error, only as quietly wrong predictions.
Worked example
Bad prompt: "Write a function that computes days since last purchase as a feature." Better prompt: "Write a Python function days_since_last_purchase(df) that takes a DataFrame with columns customer_id (string), purchase_ts (UTC timestamp, may be null for customers with no purchase history), and prediction_ts (UTC timestamp, the point in time this feature is being computed for). Return one row per customer_id with a column days_since_last_purchase (integer, null if the customer has no purchase history before prediction_ts). Only use purchases where purchase_ts < prediction_ts; never use a purchase at or after prediction_ts, since that would leak future information. Preserve customer_id as the join key and do not drop customers with no purchase history. State any assumptions about timezone handling explicitly." The second version pins the leakage boundary (purchase_ts < prediction_ts), the null-handling behavior, and the join key, none of which the first version specified.
Trade-offs and pitfalls
Writing this level of detail takes real time upfront, which can feel like overkill for a throwaway exploratory feature. The trade-off is worth it for anything that will train a production model, because the cost of finding leakage after a model is already deployed and looking suspiciously good is far higher than the cost of a careful prompt. For quick exploration, a lighter-weight prompt is fine as long as everyone treats the output as scratch code, not production-ready.
Unlock Full Question Bank
Get access to all 17 Version Control and Developer Tooling interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.