Material

What happens? Predict the code

Practice sectionPractice by format →

Predict the result and write down your reasoning first. Then run the code, check the criteria and open the explanation. 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 coach. I am solving the task myself. First ask for my prediction and reasoning; do not run the code or reveal the result. After my answer, review the reasoning, suggest one edge case and only then explain the behavior. If I ask for help, give one small hint at a time.

Scroll the table horizontally →

Assignment Stage Estimate
A shared list after copying Python basics 15 min
When a generator sees a change Python engineering 15 min
Three buttons and one closure Python engineering 15 min
Result order and completion order Async and background jobs 20 min
Where rooms without bookings went SQL and PostgreSQL 20 min

A shared list after copying

Before running the code, write the exact output of the three lists and explain when data becomes shared.

template = {"alerts": []}
first = template.copy()
second = template.copy()
first["alerts"].append("sms")
second["alerts"] = ["email"]
print(template["alerts"], first["alerts"], second["alerts"])

Check your result

  • Show all three lists in the correct order.
  • Distinguish mutating a nested list from replacing a dictionary value.
  • Show which objects share identity before and after assignment.
Hint — after your attempt

Draw the two dictionaries and arrows to their values.

Explanation — after your attempt

Output: ['sms'] ['sms'] ['email']. The shallow copies initially share one list. append mutates it; assigning the key in the second dictionary replaces only that reference.

Expected output:

['sms'] ['sms'] ['email']

Extension: Create independent preferences instead of shallow copies and test a nested dictionary.

Back to this stage

When a generator sees a change

Predict both output lines before running the code. When does the generator body execute?

events = ["queued", "ready"]
def unread():
    for value in events:
        yield value.upper()
stream = unread()
events.append("closed")
print(next(stream))
events[1] = "sent"
print(list(stream))

Check your result

  • Give the first line and remaining values in the exact order.
  • Explain when the list and each subsequent element are read.
  • Confirm that another list(stream) is empty.
Hint — after your attempt

Does the loop start at `unread()` or at `next(stream)`?

Explanation — after your attempt

First line: QUEUED; second line: ['SENT', 'CLOSED']. Iteration starts at the first next; later values come from the changed list.

Expected output:

QUEUED
['SENT', 'CLOSED']

Extension: Pass a snapshot of the list to the generator and compare the result.

Back to this stage

Three buttons and one closure

Buttons are created in a loop. What does calling every handler after the loop print?

buttons = []
for action in ("open", "save", "close"):
    buttons.append(lambda: action)
print([button() for button in buttons])

Check your result

  • Give all three values of the result.
  • Explain which variable the closures capture.
  • Suggest a separate bound value for each handler.
Hint — after your attempt

The handlers run after the loop: what is `action` then?

Explanation — after your attempt

Output: ['close', 'close', 'close']. Every handler looks up the same action variable when called. A factory argument or a default value can bind each action.

Expected output:

['close', 'close', 'close']

Extension: Rewrite the loop with a function factory and a default argument; compare behavior.

Back to this stage

Result order and completion order

Predict both output lines. The delays are used only to make completion order clear: 20 ms and 0 ms.

import asyncio

async def main():
    finished = []
    async def fetch(name, delay):
        await asyncio.sleep(delay)
        finished.append(name)
        return name.upper()
    result = await asyncio.gather(fetch("a", 0.02), fetch("b", 0))
    print(result)
    print(finished)

asyncio.run(main())

Check your result

  • Name the gather result and completion list separately.
  • Explain why the faster task does not reorder returned results.
  • State which list changes when gather arguments are swapped.
Hint — after your attempt

One list is built by `gather`; the other changes inside the coroutines.

Explanation — after your attempt

gather prints ['A', 'B'] in argument order. With these delays, finished prints ['b', 'a'] in completion order.

Expected output:

['A', 'B']
['b', 'a']

Extension: Add a third task with a delay between 0 and 20 ms and compare the orders.

Back to this stage

Where rooms without bookings went

Rooms 1, 2, 3 exist. Room 1 has one confirmed and one cancelled booking; room 2 has only a cancelled booking; room 3 has none. Which row does the query return, and why?

SELECT r.id, COUNT(b.id) AS booked
FROM rooms AS r
LEFT JOIN bookings AS b ON b.room_id = r.id
WHERE b.status = 'confirmed'
GROUP BY r.id
ORDER BY r.id;

Check your result

  • List every output row, without inventing zero rows for missing rooms.
  • Explain the effect of WHERE after a LEFT JOIN.
  • Move the status filter to a place that preserves all rooms.
Hint — after your attempt

How does comparison with `NULL` behave for rooms without a confirmed booking?

Explanation — after your attempt

The query returns only (1, 1). After the join, rooms 2 and 3 have no row matching b.status = 'confirmed'; WHERE removes them. Filtering in ON retains both rooms with zero counts.

One possible solution:

SELECT r.id, COUNT(b.id) AS booked
FROM rooms AS r
LEFT JOIN bookings AS b
  ON b.room_id = r.id AND b.status = 'confirmed'
GROUP BY r.id
ORDER BY r.id;

Extension: Move the status condition into ON and show the zero-count rows.

Back to this stage