Material

Find and fix: code review practice

Practice sectionPractice by format →

Reproduce the defect, make the smallest sound repair and verify it. Open the explanation after your attempt. All data is synthetic.

Choose your practice · Interview mistakes

Practice with an AI agent

Copy this prompt and add the chosen assignment below it:

Act as my backend reviewer. Do not fix the code for me. Ask me to reproduce the defect and explain its cause, then request a minimal patch and a check. After my attempt, assess it against the criteria, point out remaining edge cases and only then show your version. Give one small hint at a time when asked.

Scroll the table horizontally →

Assignment Stage Estimate
Notices from two guests mixed together Python basics 20 min
A stream average disappeared Python engineering 25 min
An empty room got one booking SQL and PostgreSQL 20 min
The last seat sold twice Web framework 30 min
A manual from another team is visible by ID Web framework 25 min

Notices from two guests mixed together

Two independent guests share a notice history. Reproduce the defect with two calls, repair the function and verify that an explicitly supplied list is still used.

def add_notice(message, history=[]):
    history.append(message)
    return history

Check your result

  • A second call without a list cannot see the first guest’s notice.
  • An explicitly passed list is appended to and returned under the stated contract.
  • A test catches repeated-call behavior, not only one call.
Hint — after your attempt

When is the list in the function header created?

Explanation — after your attempt

The default list is created once. Use None as a sentinel and create a list inside each call; when a list is passed, append to that list.

One possible solution:

def add_notice(message, history=None):
    if history is None:
        history = []
    history.append(message)
    return history

Extension: Compare a None sentinel with copying the passed list; choose the contract explicitly.

Back to this stage

A stream average disappeared

The function works with a list of delays but fails with a one-shot generator. Find the cause and repair it in one pass. Raise ValueError for empty input.

def average_delay(delays):
    count = sum(1 for _ in delays)
    return sum(delays) / count

Check your result

  • A list and a one-shot generator with the same values give the same result.
  • Empty input raises an explicit error.
  • The repair uses one pass and does not copy the whole stream.
Hint — after your attempt

What remains in the generator after calculating `count`?

Explanation — after your attempt

The first sum exhausts the generator. Accumulate total and count in one loop; raise ValueError if the count is zero.

One possible solution:

def average_delay(delays):
    total = 0
    count = 0
    for delay in delays:
        total += delay
        count += 1
    if count == 0:
        raise ValueError('no delays')
    return total / count

Extension: Accept a very long stream and measure extra memory.

Back to this stage

An empty room got one booking

In rooms, room 7 exists but has no bookings. The query reports one booking. Fix the counter without losing rooms from the report.

SELECT r.id, COUNT(*) AS bookings
FROM rooms AS r
LEFT JOIN bookings AS b ON b.room_id = r.id
GROUP BY r.id;

Check your result

  • A room without bookings reports 0; a room with two reports 2.
  • Explain COUNT(*) versus COUNT(b.id) after a LEFT JOIN.
  • Keep rooms with no related rows in the result.
Hint — after your attempt

How many rows does `LEFT JOIN` produce for a room without bookings?

Explanation — after your attempt

LEFT JOIN retains one room row with NULL on the right. COUNT(*) counts that row; COUNT(b.id) counts only existing bookings.

One possible solution:

SELECT r.id, COUNT(b.id) AS bookings
FROM rooms AS r
LEFT JOIN bookings AS b ON b.room_id = r.id
GROUP BY r.id;

Extension: Add a booking status and place its filter without losing empty rooms.

Back to this stage

The last seat sold twice

Two requests read available = 1 at the same time, then both sell the seat. Find the race and repair the operation so exactly one succeeds. Assume db.fetchval and db.execute each run a separate SQL command.

async def book(seat_id, db):
    available = await db.fetchval("SELECT available FROM seats WHERE id = $1", seat_id)
    if available is None or available == 0:
        return False
    await db.execute("UPDATE seats SET available = available - 1 WHERE id = $1", seat_id)
    return True

Check your result

  • For two concurrent calls and one seat, exactly one succeeds.
  • The available count cannot become negative.
  • A missing seat_id returns False; explain how update success is determined.
Hint — after your attempt

Make the availability check part of the `UPDATE` itself.

Explanation — after your attempt

Separate read and update create a race. Use one UPDATE ... WHERE id = $1 AND available > 0 RETURNING available; a returned row means success.

One possible solution:

async def book(seat_id, db):
    remaining = await db.fetchval(
        'UPDATE seats SET available = available - 1 '
        'WHERE id = $1 AND available > 0 '
        'RETURNING available',
        seat_id,
    )
    return remaining is not None

Extension: Add cancellation and ensure it cannot increase the count twice.

Back to this stage

A manual from another team is visible by ID

The user is authenticated, but can read another team’s document by knowing its ID. Repair the lookup and handle a missing document without disclosing another team’s data.

async def load_manual(manual_id, user, db):
    row = await db.fetchrow(
        "SELECT id, team_id, title FROM manuals WHERE id = $1", manual_id
    )
    if row is None:
        return None
    return dict(row)

Check your result

  • A user can read a document from their own team.
  • Foreign and missing documents reveal no data and receive the same response.
  • Enforce authorization in the server query rather than a client-side filter.
Hint — after your attempt

How can the user’s `team_id` become part of the lookup?

Explanation — after your attempt

Filtering only by id does not restrict ownership. Add AND team_id = $2 using trusted user.team_id; handle foreign and missing rows identically.

One possible solution:

async def load_manual(manual_id, user, db):
    row = await db.fetchrow(
        'SELECT id, team_id, title FROM manuals '
        'WHERE id = $1 AND team_id = $2',
        manual_id,
        user.team_id,
    )
    return None if row is None else dict(row)

Extension: Change a user’s team and verify old access.

Back to this stage