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.
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.
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.
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
gatherresult and completion list separately. - Explain why the faster task does not reorder returned results.
- State which list changes when
gatherarguments 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.
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
WHEREafter aLEFT 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.