Code Review and Working with Existing Codebases Questions
Reviewing others' code and navigating unfamiliar systems: giving and receiving actionable review feedback, spotting correctness and design issues, and reading and understanding large or legacy codebases before changing them. Covers collaborative coding norms, incremental change in shared repositories, and verifying changes against existing behavior. The team-facing side of day-to-day engineering.
Given this pandas snippet, walk through each line as you would to a junior data scientist and explain inputs, outputs, and intent:
df = df.drop_duplicates(subset=['user_id', 'event_date'])
df['event_date'] = pd.to_datetime(df['event_date'])
df = df[df['event_date'] >= '2023-01-01']
Explain potential pitfalls and one unit test to add.
Sample Answer
Direct answer
Each line does one job on the way to a clean, deduplicated, recent-events dataset: drop duplicate user-and-date rows, convert the date column from text to a real datetime type so it can be compared correctly, then filter down to events from 2023 onward. Walking a colleague through it means naming each line's input, output, and assumption, not just what the syntax does.
Structured elaboration
Line by line
df = df.drop_duplicates(subset=['user_id', 'event_date']). Input: the full dataframe (pandas' table-like data structure). Output: a dataframe with one row per unique(user_id, event_date)pair, keeping the first occurrence by default. Intent: treat repeated rows for the same user on the same date as duplicates, for example from a retried API call. Worth naming to a junior colleague: "first occurrence" is arbitrary unless the rows were sorted meaningfully first; if which duplicate survives matters, sort before deduplicating.df['event_date'] = pd.to_datetime(df['event_date']). Input: theevent_datecolumn as text. Output: the same column as an actual datetime type. Intent: text can't be compared or filtered as a date correctly for every format; converting first makes the next line's comparison mean what it looks like it means.df = df[df['event_date'] >= '2023-01-01']. Input: the dataframe with a real datetime column. Output: only rows on or after January 1, 2023. Intent: restrict analysis to a defined recent window; because the column is now a real datetime type, pandas correctly parses the string on the right for the comparison, rather than doing a plain text comparison.
Pitfalls
to_datetimeon an inconsistent or malformed date string raises an error by default rather than silently producing a wrong date, which is safer than it sounds but means the pipeline breaks hard on one bad row unless you explicitly decide how to handle it, for example converting unparseable values into a missing-date marker and then deciding whether to drop or flag them.- Deduplicating before the date conversion means "duplicate" is judged on the raw string form of the date. Two rows representing the same date but formatted differently would NOT be caught as duplicates, since the strings differ. Converting to datetime before deduplicating avoids that trap.
- The final filter creates a view that's a slice of the original; further chained assignment on it can trigger pandas' copy-versus-view warning if not handled with an explicit copy, worth flagging to a junior colleague since it's a very common pandas gotcha.
Worked example
I ran this exact three-line snippet against a small sample dataset to confirm the behavior:
import pandas as pd
df = pd.DataFrame({
'user_id': [101, 101, 102, 103, 103, 103],
'event_date': ['2022-11-05', '2022-11-05', '2023-01-15', '2023-02-01', '2023-02-01', '2023-03-10'],
'event_type': ['click', 'click', 'purchase', 'click', 'click', 'purchase'],
})
print("rows before:", len(df))
df = df.drop_duplicates(subset=['user_id', 'event_date'])
print("rows after dedup:", len(df))
df['event_date'] = pd.to_datetime(df['event_date'])
print("dtype after conversion:", df['event_date'].dtype)
df = df[df['event_date'] >= '2023-01-01']
print("rows after date filter:", len(df))
print(df)
Actual output:
rows before: 6
rows after dedup: 4
dtype after conversion: datetime64[us]
rows after date filter: 3
user_id event_date event_type
2 102 2023-01-15 purchase
3 103 2023-02-01 click
5 103 2023-03-10 purchase
Six rows go to four after deduplication (the two identical 101, 2022-11-05 rows collapse to one), then to three after the date filter drops the pre-2023 row. I separately confirmed the pitfall above is real: feeding pd.to_datetime a list containing 'not_a_date' raises a ValueError naming the exact string that failed to parse, rather than silently producing a wrong date; passing errors='coerce' instead turns that value into a missing-date marker (one row) rather than raising.
One unit test to add
def test_dedup_and_filter_drops_pre_2023_and_duplicates():
df = pd.DataFrame({
'user_id': [1, 1, 2],
'event_date': ['2022-12-31', '2022-12-31', '2023-01-01'],
})
df = df.drop_duplicates(subset=['user_id', 'event_date'])
df['event_date'] = pd.to_datetime(df['event_date'])
result = df[df['event_date'] >= '2023-01-01']
assert len(result) == 1
assert result['user_id'].iloc[0] == 2
This pins both behaviors together (the duplicate collapsed, the pre-2023 row excluded) against a known, hand-checkable expected result, so a future change to either line that breaks either behavior fails loudly instead of silently changing downstream numbers.
Trade-offs and pitfalls
- Silently dropping unparseable dates trades a hard failure for a soft one; only do this if you also log or count how many rows were affected, otherwise you lose visibility into a real data quality problem
- Deduplicating before type conversion is a subtle trap because it looks correct and usually is correct on clean data; it only breaks on inconsistently formatted date strings, exactly the kind of bug that survives review and shows up later as a mysterious discrepancy
- Comparing against a plain string relies on pandas correctly parsing it during the comparison; an explicit timestamp object is marginally more robust and easier for a junior engineer to trust at a glance
You are reviewing a data migration that renames a heavily used column and requires backfilling millions of rows. Design a rollback-safe migration strategy that can be reviewed and approved. Cover schema changes, dual-write/read strategies, backfills, verification, monitoring, and how code review should verify each migration step.
Sample Answer
Direct answer
A rollback-safe rename plus backfill never touches the old column or existing readers directly. It adds the new column alongside the old one, writes to both while backfilling the new one in batches, verifies the backfilled data matches, only then switches reads over behind a flag, and keeps the old column around for a retention window so any step can be reversed just by flipping the flag back, not by undoing a destructive change.
Structured elaboration
Each phase below names what code review should specifically confirm before approving it, as a "Review check," plus what to monitor once it ships.
1. Schema change. Add the new column as nullable, with no constraints yet:
ALTER TABLE events ADD COLUMN new_name text NULL;
Review check: confirm this specific statement is additive only and backward-compatible, meaning every existing reader and writer keeps working unmodified the moment this ships, with zero application changes required yet.
2. Dual-write. Deploy an application change, behind a feature flag, that writes both the old and new column on every write to a row. Review check: is the write to both columns transactional or otherwise guaranteed consistent (not "write old, then separately and non-atomically write new"), and is the flag off by default so this ships dormant before anything depends on it?
3. Backfill. A batched, idempotent job fills in the new column for existing rows, only where it's still NULL, ordered by primary key, with a checkpoint so it can resume after an interruption instead of restarting from row one:
UPDATE events
SET new_name = old_name
WHERE id BETWEEN :batch_start AND :batch_end
AND new_name IS NULL;
Review check: is progress persisted somewhere durable (not just in the running process's memory), and does re-running an already-completed batch do nothing (true idempotence), not create incorrect data? Monitoring: track batches completed, rows backfilled, replication lag, and write error rate on the table for the duration of the backfill, and pause automatically if replication lag crosses an agreed threshold.
4. Verification. Before trusting the backfill, sample a random set of rows and confirm new_name matches what old_name implies for each. Review check: is the sample size and comparison method actually specified in the PR, not just asserted as "we verified it"?
5. Read cutover. Only after verification passes, flip the flag so reads prefer the new column, falling back to the old one if the new one is somehow still empty for a given row. Review check: is the fallback logic actually tested, not just written? Monitoring: application error rate and query latency on this table specifically, right after the flag flips, since a regression here is the trigger for the rollback shown in the diagram below.
6. Cleanup. Once reads have run on the new column successfully for a defined retention window, make it NOT NULL, add any index it needs, and only then drop the old column, in a separate, later PR. Review check: is dropping the old column genuinely a separate step from everything above, so it can never accidentally ship bundled with a change that hasn't been verified yet?
flowchart LR
A[Step 1: add new_name column, nullable] --> B[Step 2: dual write old_name plus new_name]
B --> C[Step 3: backfill new_name in batches where NULL]
C --> D{Verification: sampled row values match}
D -- mismatch found --> C
D -- fully verified --> E[Step 4: flip reads to new_name behind a flag]
E --> F{Error rate normal after cutover}
F -- regression --> G[Rollback: flip flag back to old_name, dual write stays intact]
F -- healthy --> H[Step 5: make new_name NOT NULL, add index concurrently]
H --> I[Step 6: drop old_name after a retention window]
Worked example
Renaming user_email to primary_email on a table with 40 million rows, backfilled in batches of 5,000 rows: that's 40,000,000 / 5,000 = 8,000 batches total. With a short pause between batches to keep replication lag bounded, the job runs as a background process over however long it takes to work through all 8,000 batches, checkpointing its position after each one so a restart resumes from the last completed batch instead of row one. Verification samples 10,000 random rows after the backfill reports complete and confirms primary_email equals user_email for every one of them before the flag is ever flipped to prefer reads from the new column.
Trade-offs and pitfalls
Every step here is reversible specifically because the old column and old read path stay intact until the very last, separate cleanup step, which is exactly what makes this slower and more code than a single rename statement; that trade is worth it for a heavily-used column and wrong for a rarely-touched internal table, where a single migration with a maintenance window might be simpler and perfectly safe. The most dangerous version of this pattern to review is one that quietly combines two of these steps, most often shipping the read cutover and the old-column drop in the same change, which collapses the rollback safety the whole design exists to provide.
Describe concrete tactics for using code reviews as a mentoring tool for junior engineers. Include how you structure comments, what to pair-program versus comment, how to provide targeted learning resources, and how to measure progress over time for the mentee.
Sample Answer
Direct answer
Treat code review as one of several teaching tools, not the only one: use written comments for things that are efficient to explain in text, switch to pairing for anything that needs real back-and-forth, and track whether the same class of comment keeps recurring as the honest signal of whether the mentee is learning, not just complying.
Structured elaboration
Structuring comments
Lead with the "why," not just "change this to that." Separate must-fix from optional or learning-opportunity comments explicitly, so the mentee isn't guessing at severity. Ask a question ("what happens if this list is empty?") instead of dictating the fix when the goal is for them to reason through it themselves; state it directly when time pressure or risk is high enough that the learning moment can wait.
Pair-programming versus commenting
Pair when the concept is genuinely new to them, a new pattern or a new part of the codebase, or when a comment thread has gone back and forth more than twice without converging. Use written comments for anything they've seen before and just need a nudge on, since it's asynchronous and doesn't interrupt their flow.
Targeted learning resources
Link to the specific doc, prior PR (pull request), or style-guide section that addresses the exact gap, not a generic "read the docs on X." Even better: point to a real example already in the codebase that does it well, since it's concrete and has already passed review.
Measuring progress
Track whether the same category of comment (e.g. "add error handling," "extract this function") shows up less often across their later PRs. A good sign is the mentee starting to anticipate the class of feedback you'd give and addressing it before you comment. A bad sign is the same class of feedback repeating PR after PR with no change in how the code arrives.
Worked example
Mentoring a junior engineer over a couple of months: early PRs draw frequent comments on missing error handling and untested edge cases. When they hit an unfamiliar part of the codebase (an async job queue), that becomes a pairing session rather than a comment thread, since it's genuinely new. For a recurring "extract this into a function" pattern, the mentor points them at a specific earlier PR in the codebase that does it well, rather than a generic style guide link. The measurable, honest signal by the third or fourth PR: the error-handling comments mostly stop appearing, and their tests start covering edge cases unprompted, not a fabricated precise percentage.
Trade-offs and pitfalls
Mentoring through review can tip into micromanaging, rewriting their solution in comments instead of letting them arrive at it themselves. It can also become one-sided, where the mentor never learns anything from the mentee's perspective on the code. A common wrong turn is being so gentle that a genuinely blocking issue reads as optional and ships anyway, which helps no one.
You are reviewing a pull request that normalizes features with a manual loop. The diff shows a Python function that scales each column in a loop instead of vectorized operations. Provide constructive review comments focusing on performance, readability, edge cases, and test suggestions. Include a concrete code suggestion or alternative approach.
Sample Answer
Direct answer
The core review comment is that a Python-level per-row loop over a table is both slower and more error-prone than a vectorized operation (one that operates on a whole column at once instead of cell by cell), and this specific diff also has a real correctness bug: dividing by a zero-span constant column, plus a dtype issue I reproduced directly. I'd ask for the loop to be replaced with a vectorized min-max scale and a test covering the constant-column edge case.
Structured elaboration
Performance
Iterating with row-by-row indexing inside a Python for loop touches the table one cell at a time through the library's indexing machinery on every iteration, instead of letting the underlying vectorized engine operate on the whole column as a single operation. The fix is a vectorized subtract-and-divide across the columns at once, computed as one operation on the underlying array rather than one Python-level operation per row.
Readability
The loop version buries the actual formula (min-max scaling) inside indexing boilerplate; a reviewer has to mentally simulate the loop to recover what the code is doing. The vectorized version reads as the formula itself, which is what a reviewer, and a future maintainer, actually wants to verify is correct.
Edge cases
- Constant column (max equals min): the formula divides by zero. I verified this concretely: an unguarded vectorized version on a constant column produces a missing-value marker for every entry rather than raising, which is silent and easy to miss. This needs an explicit decision (skip the column, fill with a default, or raise a clear error) rather than an accidental one.
- Integer-typed columns: I actually ran the loop version against integer-typed columns and it raised a type error, because assigning a fractional result back into an integer column cell by cell isn't allowed. This is a real, reproducible bug in the diff as written, not a hypothetical: any caller passing an integer-typed feature column hits it immediately. The vectorized version needs the same fix: cast the columns to a floating-point type before scaling.
- Missing values already present: neither version currently guards against a missing value in the input propagating through the min, max, and arithmetic; worth a comment asking whether that's intended.
Concrete suggestion
import numpy as np
def normalize_features(df, columns):
df = df.copy()
df[columns] = df[columns].astype(float)
col_min = df[columns].min()
col_max = df[columns].max()
span = (col_max - col_min).replace(0, np.nan) # avoid divide-by-zero; NaN signals "cannot scale a constant column"
df[columns] = (df[columns] - col_min) / span
return df
Test suggestions
- A normal case with known min/max per column, asserting the scaled values match hand-computed expected values
- A constant column, asserting it comes out as a missing-value marker (or whatever the team agrees on) rather than raising or silently returning garbage
- An integer-typed input column, asserting it doesn't raise, which would have caught the dtype bug above
Worked example
I ran both the original loop version and the suggested vectorized version against the same float-typed sample data to confirm they produce identical results, and separately confirmed the two edge cases above.
import pandas as pd
import numpy as np
def normalize_features_loop(df, columns):
df = df.copy()
for col in columns:
col_min = df[col].min()
col_max = df[col].max()
for i in range(len(df)):
val = df.iloc[i][col]
df.iloc[i, df.columns.get_loc(col)] = (val - col_min) / (col_max - col_min)
return df
def normalize_features_vectorized(df, columns):
df = df.copy()
col_min = df[columns].min()
col_max = df[columns].max()
span = (col_max - col_min).replace(0, np.nan)
df[columns] = (df[columns] - col_min) / span
return df
df = pd.DataFrame({
'feature_a': [10, 20, 30, 40],
'feature_c': [1, 2, 3, 100],
}).astype(float)
loop_result = normalize_features_loop(df, ['feature_a', 'feature_c'])
vec_result = normalize_features_vectorized(df, ['feature_a', 'feature_c'])
print(np.allclose(loop_result[['feature_a', 'feature_c']], vec_result[['feature_a', 'feature_c']]))
print(vec_result)
Actual output:
True
feature_a feature_c
0 0.000000 0.000000
1 0.333333 0.010101
2 0.666667 0.020202
3 1.000000 1.000000
Both versions agree exactly on this data. I also confirmed the dtype pitfall is real: calling the loop version on the original integer-typed columns, without the cast to float first, raises a type error naming the specific fractional value it couldn't place into an integer column, on the very first row where the scaled value isn't a whole number.
Trade-offs and pitfalls
- A vectorized fix that skips the dtype cast just moves the same bug from "raises inside a loop" to "raises, or silently truncates depending on the library version, on the whole-column assignment"; the fix has to address the dtype, not only the loop
- Silently turning a constant column into a missing-value marker is defensible but must be a decision the team agrees on, since a downstream model silently receiving missing features can fail in a much more confusing way, far from this code
- Vectorization sometimes trades a small amount of readability for a large amount of speed on genuinely large data, but here the vectorized version is also more readable, so there's no real trade-off to accept in this specific case
Design a peer-review rubric for ETL or transformation pull requests that balances velocity and reliability. List 6-8 rubric items (for example, tests, readability, performance considerations) and explain how each item should be scored and enforced in the review process.
Sample Answer
A peer-review rubric for extract, transform, load (ETL) or transformation pull requests has to score two different things at once: will this data be reliable, and how much reviewer time will this actually cost. I use seven items on a 0-2 scale (0 = missing, 1 = partial, 2 = complete), split between reliability items that gate the merge and delivery-health items that stay visible but do not block on their own. Whatever can be checked by a machine is enforced in continuous integration (CI); whatever needs judgment goes to a human reviewer.
The seven items
- Tests: 0 = none, 1 = unit tests only, 2 = unit plus an integration run against sample data. CI-enforced; unit tests block the merge, integration tests are required for anything touching a production table.
- Data-quality assertions: 0 = none, 1 = basic null/uniqueness checks, 2 = ranges, referential integrity (a foreign-key-style reference still points to a row that actually exists), and anomaly checks. CI-enforced; a failing assertion must be fixed or explicitly justified with a mitigation plan in the PR description.
- Schema and contract changes: 0 = undocumented or breaking with no migration, 1 = documented with no migration plan, 2 = documented with a migration or backfill plan and backward compatibility. Reviewer-enforced; a breaking change needs a second approver.
- Readability and maintainability: 0 = hard to follow, 1 = readable but undocumented, 2 = clear, modular, with docstrings. Style is linted in CI; architectural clarity is a human judgment call.
- Performance and resource cost: 0 = untested and likely expensive, 1 = a rough estimate, 2 = profiled with cost or latency numbers included. Reviewer-enforced for anything touching a heavy job.
- Observability: 0 = no metrics or logs, 1 = basic logs, 2 = row-count and latency metrics with an updated dashboard or alert. Reviewer-enforced; absence requires a documented follow-up.
- Security and access: 0 = sensitive data handled unsafely, 1 = encryption or access control where needed, 2 = approved masking and least-privilege access reviewed. CI checks for hardcoded secrets; a security owner signs off on anything touching regulated data.
Worked example
Consider a PR that adds a transformation masking a customer email column before it lands in an analytics table, on a dataset of moderate size. Scoring it: tests = 2 (unit tests plus a small integration run), data-quality = 1 (adds a null check but no uniqueness or range assertion), schema = 2 (documented, includes a backfill plan because it changes an existing column), readability = 2, performance = 1 (small change, no profiling numbers attached), observability = 0 (no new metric added for the masking step), security = 2 (masking is exactly what this item exists to reward). Total: 2 + 1 + 2 + 2 + 1 + 0 + 2 = 10 out of a possible 14. Under a threshold like "10 or higher clears with one reviewer," this PR passes with a single approver, but the reviewer should still call out the observability zero explicitly as a fast-follow rather than let a strong total quietly absorb it.
Trade-offs and pitfalls
A pure point-sum threshold can hide exactly that kind of zero on a critical item, so pair the total with a hard floor rule: any single 0 on tests or data-quality forces reviewer sign-off regardless of total. A rubric that is too granular also becomes its own overhead, where reviewers spend more time scoring than reviewing; keep CI-enforceable items automated so a human is only asked to judge genuinely subjective items like readability. Finally, the same rubric should not apply uniformly everywhere: a lighter version fits exploratory or one-off pipelines, and the full rubric belongs on anything that feeds a production or shared table.
Unlock Full Question Bank
Get access to all 12 Code Review and Working with Existing Codebases interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.