Practice production-shaped Python coding prompts: crawlers, in-memory stores, ledgers, schedulers, parsers, rate limiters, caches, and concurrency follow-ups.
After the system design capstones, the final section turns architecture knowledge into interview execution. The first test is whether you can build small, correct systems while requirements change.
AI lab coding formats vary by team. Public frontier-lab guidance emphasizes well-designed solutions, tests, live coding, debugging, standard-library fluency, experience, motivation, communication, and tradeoff analysis.[1][2] Practice that bar with one base prompt followed by staged requirements: add TTLs, add concurrency, add cancellation, add rate limits, preserve deterministic output, or explain why your state model won't corrupt itself.
The final interview-prep section ends with a coding session. Build speed at practical Python: clear state, small APIs, local tests, and honest concurrency invariants.
Treat the final prep section as one packet. Each round tests a different artifact, but the artifacts should agree with each other.
| Round | Pattern families | Proof artifact |
|---|---|---|
| Coding | traversal, stores, TTL, ordering, parsing, concurrency, validation | passing tests plus an invariant you can explain under follow-up |
| System design | model gateway, retrieval, scheduler, coding agent, eval rollout | API, data model, scale math, overload plan, permission boundary |
| Behavioral | motivation, risk judgment, disagreement, ambiguity, failure, growth | five story bank with metrics, artifacts, and honest reflections |
| Technical presentation | architecture narrative, tradeoffs, metrics, AI bridge, Q&A defense | 15-minute talk, 5-minute version, appendix, ownership boundary |
Use the map to avoid uneven prep. A strong candidate doesn't only solve Python prompts; they can also explain why a design boundary exists, what failure changed their judgment, and which project artifact proves depth.
Use this loop for every prompt:
Good interview code isn't the most abstract code. It's code whose invariants can be defended while requirements change.
Know these without documentation:
| Need | Python building block |
|---|---|
| FIFO work queue | collections.deque, queue.Queue, asyncio.Queue |
| Counts and top errors | collections.Counter |
| LRU cache | collections.OrderedDict |
| Deadlines and TTL | time.monotonic, injected now function |
| Priority scheduling | heapq, queue.PriorityQueue |
| Thread safety | threading.Lock, threading.RLock, threading.Condition, threading.Event |
| Worker fanout | concurrent.futures.ThreadPoolExecutor, as_completed |
| Parsing | splitlines, re, explicit state machines |
Don't wait for a test framework. Write a small run_tests() and use plain assert.
Use these as drills. Don't memorize wording. Learn the patterns.
| Prompt pattern | Base implementation | Follow-ups |
|---|---|---|
| Same-host web crawler | BFS/DFS with visited set | concurrency, per-host rate limit, timeouts, cancellation |
| In-memory key/value DB | set, get, delete, scan | TTL, compare-and-set, transactions, snapshots |
| Banking ledger | accounts, deposit, withdraw, transfer | idempotency, reversals, scheduled transfers, deadlock avoidance |
| Task scheduler | dependency graph and ready queue | cycle detection, retries, worker pool, cancellation |
| Log parser | multiline event grouping | malformed lines, rolling windows, top errors |
| Rate limiter | fixed or sliding window | token bucket, multi-dimensional quotas, retry-after |
| LRU/TTL cache | capacity eviction | TTL, thread safety, metrics, stale cleanup |
| LFU/cache policy | frequency buckets and recency tie-break | TTL, capacity 0, update semantics, thread safety |
| Stream assembler | chunks, sequence IDs, end markers | out-of-order chunks, duplicate chunks, timeout, memory cap |
| Schema validator | nested dict/list validation | defaults, unknown fields, coercion, error paths |
| Batch scheduler | queue by arrival and priority | fairness, deadlines, retries, starvation |
| Secret redactor | token scanning and replacement | overlapping matches, allowlists, multiline logs |
Use this as the main circuit. Each linked drill carries a full prompt, clarification questions, sample tests, hidden tests, solution guide, and validated Python plus Java paths. Run each problem twice: first in Python for speed, then in Java to force explicit classes, maps, and boundary checks.
For every practice run, write these artifacts before opening the guide:
assert tests.That routine makes LeetLLM practice closer to a real frontier coding loop: define semantics, ship correct code, prove boundaries, then extend without rewriting the state model.
Most prompts are one of these shapes. Classify the prompt before writing code.
| Pattern | Recognition signal | First state model | High-value tests |
|---|---|---|---|
| Traversal | URLs, graph nodes, dependencies, neighbors | visited plus queue or stack | cycle, duplicate edge, malformed node, deterministic order |
| Mutable store | set, get, delete, scan, inventory operations | dict from key to record | missing key, overwrite, delete, scan ordering, expired key |
| Time boundary | TTL, deadline, rate limit, retry-after | injected now, deadline fields | equality boundary, refill edge, expired-on-read, no sleep |
| Ordering policy | priority, deadline, LRU, LFU, ready queue | heap, deque, OrderedDict, frequency buckets | tie-break, stale heap entry, capacity 0, promotion |
| Idempotent write | request ID, retry, ledger, external action | request key to stored result | duplicate success, duplicate failure, conflicting retry |
| Parser | logs, events, chunks, sections, streaming lines | explicit current record or buffer | malformed line, continuation, empty input, final flush |
| Concurrency | workers, thread-safe, fanout, cancellation | lock-protected claim point plus queue | duplicate claim, shutdown, exception, partial result |
| Validation | schema, payload, permissions, filters | recursive validator or rule table | nested failure, path reporting, unknown field, missing required |
Use this sentence when stuck:
The invariant is
X; this data structure makesXcheap to maintain; these tests prove the boundary cases.
Use the same rhythm for every timed solution:
For a frontier-lab coding round, communication is part of correctness. Say when you're making an assumption, say which invariant you're protecting, and say what test you'll write before you write it.
| Session | Drill set | Goal |
|---|---|---|
| 1 | crawler, filesystem, tokenizer | traversal and parsing without losing order |
| 2 | TTL store, token bucket, retry planner | deterministic time and boundary tests |
| 3 | LRU, LFU, priority scheduler | eviction, tie-breaks, and stale heap entries |
| 4 | ledger, workflow state machine, schema validator | idempotency, transitions, and explainable errors |
| 5 | worker queue, batch scheduler, stream assembler | concurrency, backpressure, and partial results |
| 6 | two random problems under 35 minutes each | speed, tests, and clean explanation |
Many frontier coding rounds feel like one product-shaped problem that grows across levels. Practice each ladder by shipping level 1 quickly, then preserving the same invariant as requirements change.
| Prompt family | Level 1 | Level 2 | Level 3 | Level 4 |
|---|---|---|---|---|
| GPU quota reservation system | add, remove, lookup | reserve/release capacity | expirations, over-allocation prevention | concurrency or audit log |
| Key/value store | set, get, delete | prefix scan and ordering | TTL and compare-and-set | transactions or snapshots |
| Crawler | same-host BFS | URL normalization | retries and rate limits | worker fanout and cancellation |
| Scheduler | dependencies and ready queue | cycle detection | retries and deadlines | workers, cancellation, fairness |
| Cache | get/put capacity eviction | update semantics | TTL or LFU | metrics and thread safety |
| Chat or event router | register handlers | route messages | priorities or filters | replay, idempotency, backpressure |
| Log processor | parse records | multiline events | top errors and windows | malformed input and streaming |
| Permission filter | include/exclude resources | groups and inheritance | deny precedence | audit why each item passed |
| Stream assembler | append chunks | sequence ordering | duplicate/missing chunks | timeout and memory cap |
| Experiment splitter | assign users | stable hashing | ramp percentages | sticky overrides and rollback |
Use this timing target:
| Time | Target |
|---|---|
| 0-5 min | clarify ordering, failure behavior, and mutable state |
| 5-20 min | level 1 complete with tests |
| 20-32 min | level 2 or 3 complete without rewriting |
| 32-38 min | edge tests and complexity |
| final minutes | name next follow-up design, race risk, and production hardening |
Score yourself after each drill.
| Signal | Not ready | Ready | Strong |
|---|---|---|---|
| Clarification | starts coding before semantics | asks about order, missing inputs, equality boundary | predicts which answer changes tests |
| Data model | grows incidental state | names records and invariants | can extend same model across levels |
| Tests | only happy path | missing, duplicate, boundary, and sample tests | tests one future follow-up before code |
| Standard library | reinvents queues, counters, heaps, caches | uses obvious standard tool | explains why that tool fits invariant |
| Debugging | stares at failure | narrows with print/assert/repro | states expected vs actual before editing |
| Follow-up handling | rewrites from scratch | changes one boundary at a time | names tradeoff and reversal signal |
| Communication | quiet implementation | narrates assumptions and complexity | keeps interviewer aligned through decisions |
When a drill scores "not ready," repeat the same prompt family with different nouns. Train transfer, not memorization.
State the tests before coding the follow-up. That keeps the interviewer aligned and prevents late rewrites.
| Pattern | Minimum public tests | Private-edge tests to rehearse |
|---|---|---|
| Crawler or graph traversal | start node, duplicate edge, off-host or invalid neighbor | cycles, relative links, malformed URL, empty graph, deterministic order |
| TTL store or rate limiter | set/get, expiry, scan, refill | equality boundary, zero TTL, time moving backward, cleanup during read |
| LRU or LFU cache | capacity eviction, update existing key, get promotion | capacity 0, tie-break by recency, expired entry, stale heap record |
| Scheduler | independent tasks, dependency release, retry | cycle, missing dependency, permanent failure, blocked dependents |
| Ledger | deposit, withdraw, transfer | duplicate idempotency key, conflicting retry, insufficient funds, lock order |
| Parser | one record, multiline continuation, malformed line | orphan continuation, empty input, final flush, very large record |
| Validator | required field, unknown field, nested list | error path, default value, type coercion, repeated failure |
| Stream assembler | in-order chunks, out-of-order chunks | duplicate chunk, missing end marker, timeout, memory cap |
Use this sentence before running tests:
I expect these tests to fail if I lose the invariant. The invariant is
X; the edge case most likely to break it:Y.
When a test fails, narrate the smallest useful investigation:
Common debug pivots:
| Symptom | First thing to inspect |
|---|---|
| Duplicate output | claim point or visited insertion timing |
| Missing output | enqueue condition, prefix filter, or failure policy |
| Wrong order | queue/heap tie-break and where sorting happens |
| Expired key returned | read path skipped cleanup |
| Retry ran too many times | attempt counter increment point |
| Blocked task marked failed | dependency failure policy mixed with task execution failure |
| Threaded result flakes | shared state changed outside the lock |
Strong candidates aren't bug-free. They recover fast because they can name which invariant is broken.
Practice short narration so you don't go silent while thinking. Use these sentences as scaffolding, then replace the placeholders with prompt-specific details.
| Moment | What to say |
|---|---|
| Before coding | "I will first ship the single-threaded version, then add follow-up without changing the core invariant." |
| Choosing data structures | "The cheap operation needs to be operation, so I will store state as structure." |
| Before tests | "I want tests for happy path, duplicate input, boundary condition, and malformed input." |
| On failure | "Expected X, got Y. The likely owner is state transition, so I will inspect that before editing." |
| Adding TTL or deadlines | "I will inject time so expiry tests don't sleep and the equality boundary is explicit." |
| Adding concurrency | "The shared state is X; the claim point is Y; work runs outside the lock." |
| Running out of time | "The base invariant is correct. The next production hardening step is race/cleanup/backpressure." |
When the interviewer adds a follow-up, classify it before changing code.
| Follow-up type | First move | Common trap |
|---|---|---|
| Time | inject now, store deadline, clean on read | sleeping in tests |
| Ordering | choose deque, heap, sort, or insertion order | mixing policy into unrelated state |
| Capacity | evict by explicit rule | hidden off-by-one at capacity 0 or 1 |
| Retry | store attempt count and final state | retrying external writes without idempotency |
| Snapshot | version records or copy-on-write | mutating data a snapshot should freeze |
| Compare-and-set | lock compare and write together | calling get then set as two operations |
| Thread safety | protect claim point and shared maps | holding lock during slow fetch or execution |
| Cancellation | durable flag plus cooperative checks | stopping new work while workers keep publishing children |
Good follow-up handling sounds calm because the state boundary is still visible.
Use these as timed practice. Read the prompt, write a small solution and tests, then open the solution guide.
You're given a starting URL and a function get_links(url) -> list[str]. Implement a crawler that visits only URLs on the same host as the start URL.
Requirements:
visited.Clarifying questions to ask:
Use a FIFO work queue for the base version because the prompt asks for breadth-first order. Put URL normalization and host checks before enqueueing. The invariant is: a URL enters the queue only after it has been accepted into visited.
1from collections import deque
2from urllib.parse import urlparse, urljoin
3
4PAGES = {
5 "https://repo.example/start": ["/tests", "/ci", "https://other.example/x"],
6 "https://repo.example/tests": ["/ci"],
7 "https://repo.example/ci": [],
8}
9
10def crawl(start_url: str, get_links) -> list[str]:
11 start = urlparse(start_url)
12 if not start.scheme or not start.netloc:
13 return []
14
15 visited = {start_url}
16 queue = deque([start_url])
17 ordered: list[str] = []
18
19 while queue:
20 url = queue.popleft()
21 ordered.append(url)
22
23 for raw_link in get_links(url):
24 candidate = urljoin(url, raw_link)
25 parsed = urlparse(candidate)
26 if parsed.scheme not in {"http", "https"}:
27 continue
28 if parsed.netloc != start.netloc:
29 continue
30 if candidate in visited:
31 continue
32 visited.add(candidate)
33 queue.append(candidate)
34
35 return ordered
36
37print(crawl("https://repo.example/start", lambda url: PAGES.get(url, [])))1['https://repo.example/start', 'https://repo.example/tests', 'https://repo.example/ci']For worker fanout, say explicitly that deterministic return order becomes expensive unless the prompt still requires it. Protect the claim step, not the whole fetch:
1from threading import Lock
2
3visited = {"https://repo.example/start"}
4visited_lock = Lock()
5
6def claim(candidate: str) -> bool:
7 with visited_lock:
8 if candidate in visited:
9 return False
10 visited.add(candidate)
11 return True
12
13print(claim("https://repo.example/tests"))
14print(claim("https://repo.example/tests"))
15print(sorted(visited))1True
2False
3['https://repo.example/start', 'https://repo.example/tests']For rate limiting, add a per-host token bucket or next-allowed timestamp before fetch. For cancellation, check a threading.Event before scheduling new work, before fetching, and after each fetch before enqueueing children. The full answer should name all three behaviors: duplicate prevention, bounded fetch concurrency, and graceful stop.
Implement an in-memory store with set(key, value, ttl=None), get(key), delete(key), and scan(prefix).
Requirements:
ttl is measured in seconds.sleep.Clarifying questions to ask:
scan(prefix) return keys, values, or key/value pairs?expires_at == now, or only when expires_at < now?Inject now so tests can advance time directly. Store expires_at beside each value. The invariant is: every public read path either returns a non-expired value or removes the expired key.
1from dataclasses import dataclass
2from typing import Callable
3
4@dataclass
5class Entry:
6 value: str
7 expires_at: float | None
8
9class Store:
10 def __init__(self, now: Callable[[], float]) -> None:
11 self.now = now
12 self.items: dict[str, Entry] = {}
13
14 def set(self, key: str, value: str, ttl: float | None = None) -> None:
15 expires_at = None if ttl is None else self.now() + ttl
16 self.items[key] = Entry(value, expires_at)
17
18 def get(self, key: str) -> str | None:
19 entry = self.items.get(key)
20 if entry is None:
21 return None
22 if entry.expires_at is not None and entry.expires_at <= self.now():
23 self.items.pop(key, None)
24 return None
25 return entry.value
26
27 def delete(self, key: str) -> None:
28 self.items.pop(key, None)
29
30 def scan(self, prefix: str) -> list[str]:
31 return sorted(key for key in list(self.items) if key.startswith(prefix) and self.get(key) is not None)
32
33clock = {"now": 10.0}
34store = Store(lambda: clock["now"])
35store.set("task:1", "running", ttl=5.0)
36store.set("task:2", "ready")
37print(store.get("task:1"), store.scan("task:"))
38clock["now"] = 15.0
39print(store.get("task:1"), store.scan("task:"))1running ['task:1', 'task:2']
2None ['task:2']For compare-and-set, clean any expired value and compare while holding the same lock. The compact example below omits TTL so the atomic boundary is easy to see:
1from threading import RLock
2
3class AtomicStore:
4 def __init__(self) -> None:
5 self.items: dict[str, str] = {}
6 self.lock = RLock()
7
8 def compare_and_set(self, key: str, expected: str | None, value: str) -> bool:
9 with self.lock:
10 if self.items.get(key) != expected:
11 return False
12 self.items[key] = value
13 return True
14
15store = AtomicStore()
16print(store.compare_and_set("task:1", None, "running"))
17print(store.compare_and_set("task:1", None, "ready"))
18print(store.compare_and_set("task:1", "running", "ready"))1True
2False
3TrueFor the TTL store, wrap every public method with the same lock. If compare_and_set() calls get() and set() while holding that lock, use an RLock or split the internal helpers so the lock is acquired once. The important answer is atomicity: no other thread can change the key between compare and set.
Implement a scheduler for tasks with dependencies. A task becomes runnable when all dependencies have completed.
Requirements:
max_attempts.Clarifying questions to ask:
Represent the graph explicitly: dependents[task] points to tasks released by this task, and remaining[task] counts unmet dependencies. Use heapq for priority. The invariant is: a task enters the heap only when remaining[task] == 0.
1import heapq
2from collections import defaultdict
3from dataclasses import dataclass
4
5@dataclass(frozen=True)
6class Task:
7 name: str
8 priority: int
9 deps: tuple[str, ...] = ()
10
11def schedule(tasks: list[Task], run, max_attempts: int = 2) -> tuple[list[str], list[str], list[str]]:
12 if max_attempts <= 0:
13 raise ValueError("max_attempts must be positive")
14 by_name = {task.name: task for task in tasks}
15 if len(by_name) != len(tasks):
16 raise ValueError("duplicate task name")
17 dependents: dict[str, list[str]] = defaultdict(list)
18 remaining = {task.name: len(task.deps) for task in tasks}
19
20 for task in tasks:
21 for dep in task.deps:
22 if dep not in by_name:
23 raise ValueError(f"unknown dependency: {dep}")
24 dependents[dep].append(task.name)
25
26 state: dict[str, int] = {}
27
28 def visit(name: str) -> None:
29 marker = state.get(name, 0)
30 if marker == 1:
31 raise ValueError("cycle")
32 if marker == 2:
33 return
34 state[name] = 1
35 for dep in by_name[name].deps:
36 visit(dep)
37 state[name] = 2
38
39 for name in by_name:
40 visit(name)
41
42 ready = [(task.priority, task.name) for task in tasks if remaining[task.name] == 0]
43 heapq.heapify(ready)
44 attempts = defaultdict(int)
45 completed: list[str] = []
46 failed: list[str] = []
47
48 while ready:
49 _, name = heapq.heappop(ready)
50 attempts[name] += 1
51 if not run(name):
52 if attempts[name] < max_attempts:
53 heapq.heappush(ready, (by_name[name].priority, name))
54 else:
55 failed.append(name)
56 continue
57
58 completed.append(name)
59 for child in dependents[name]:
60 remaining[child] -= 1
61 if remaining[child] == 0:
62 heapq.heappush(ready, (by_name[child].priority, child))
63
64 blocked = sorted(set(by_name) - set(completed) - set(failed))
65 return completed, failed, blocked
66
67tasks = [
68 Task("pack", priority=2),
69 Task("ship", priority=3, deps=("pack",)),
70 Task("label", priority=1),
71]
72fail_once = {"pack"}
73
74def run_with_one_retry(name: str) -> bool:
75 if name in fail_once:
76 fail_once.remove(name)
77 return False
78 return True
79
80print(schedule(tasks, run_with_one_retry))
81print(schedule([Task("pack", 1), Task("ship", 2, ("pack",))], lambda _: False, max_attempts=1))1(['label', 'pack', 'ship'], [], [])
2([], ['pack'], ['ship'])Keep permanently failed tasks separate from blocked dependents. A blocked task never ran; callers may skip it, surface the failed prerequisite, or retry after repair.
For fanout, keep graph construction and cycle detection single-threaded. Then protect only shared scheduler state: ready heap, attempts, completed, failed, and remaining dependency counts. Worker threads can run tasks outside the lock, then reacquire the lock to publish success/failure and release dependents.
If asked about retries, say whether retries preserve priority or use backoff. A concise full answer: "Ready tasks are claimed under a condition variable, execution happens outside the lock, and completion updates notify workers when new tasks become ready."
Rate limiters show up because they combine state, boundary conditions, and production behavior. Retry policies often need jitter to avoid synchronized retry storms, so this prompt is really about overload control as much as counters.[3] A strong implementation injects time so tests don't sleep.
1import math
2from dataclasses import dataclass
3
4@dataclass
5class Bucket:
6 capacity: float
7 refill_per_second: float
8 tokens: float
9 updated_at: float
10
11class TokenBucketLimiter:
12 def __init__(self, capacity: int, refill_per_second: float) -> None:
13 if capacity <= 0:
14 raise ValueError("capacity must be positive")
15 if not math.isfinite(refill_per_second) or refill_per_second <= 0:
16 raise ValueError("refill_per_second must be positive and finite")
17 self.capacity = float(capacity)
18 self.refill_per_second = float(refill_per_second)
19 self._buckets: dict[str, Bucket] = {}
20
21 def allow(self, key: str, now: float, cost: float = 1.0) -> tuple[bool, float]:
22 if not math.isfinite(now):
23 raise ValueError("now must be finite")
24 if not math.isfinite(cost) or not 0 < cost <= self.capacity:
25 raise ValueError("cost must be greater than zero and no larger than capacity")
26 bucket = self._buckets.get(key)
27 if bucket is None:
28 bucket = Bucket(self.capacity, self.refill_per_second, self.capacity, now)
29 self._buckets[key] = bucket
30 if now < bucket.updated_at:
31 raise ValueError("now must not move backwards")
32
33 elapsed = max(0.0, now - bucket.updated_at)
34 bucket.tokens = min(bucket.capacity, bucket.tokens + elapsed * bucket.refill_per_second)
35 bucket.updated_at = now
36
37 if bucket.tokens >= cost:
38 bucket.tokens -= cost
39 return True, 0.0
40
41 missing = cost - bucket.tokens
42 retry_after = missing / bucket.refill_per_second
43 return False, retry_after
44
45limiter = TokenBucketLimiter(capacity=3, refill_per_second=1.0)
46print([limiter.allow("org-a", now=0.0)[0] for _ in range(4)])
47print(limiter.allow("org-a", now=0.5))
48print(limiter.allow("org-a", now=1.0))
49try:
50 limiter.allow("org-a", now=2.0, cost=4.0)
51except ValueError as error:
52 print(error)1[True, True, True, False]
2(False, 0.5)
3(True, 0.0)
4cost must be greater than zero and no larger than capacityWhat to say out loud:
now is injected for deterministic tests._buckets and bucket mutation.A crawler prompt tests graph traversal plus concurrency. The invariant is simple: each URL is claimed once before it's fetched or enqueued.
1from collections import deque
2from urllib.parse import urlparse, urljoin
3
4PAGES = {
5 "https://lab.example/start": ["/a", "/b", "https://other.example/x"],
6 "https://lab.example/a": ["/b", "/c"],
7 "https://lab.example/b": ["/c"],
8 "https://lab.example/c": [],
9}
10
11def get_urls(url: str) -> list[str]:
12 return PAGES.get(url, [])
13
14def same_host_crawl(start_url: str) -> list[str]:
15 start_host = urlparse(start_url).netloc
16 queue = deque([start_url])
17 visited = {start_url}
18 ordered: list[str] = []
19
20 while queue:
21 url = queue.popleft()
22 ordered.append(url)
23 for raw_link in get_urls(url):
24 link = urljoin(url, raw_link)
25 if urlparse(link).netloc != start_host:
26 continue
27 if link in visited:
28 continue
29 visited.add(link)
30 queue.append(link)
31
32 return ordered
33
34print(same_host_crawl("https://lab.example/start"))1['https://lab.example/start', 'https://lab.example/a', 'https://lab.example/b', 'https://lab.example/c']Concurrency follow-up:
visited with a lock.Ledger prompts test whether you can keep money-like state consistent. Use append-only events when possible; if you maintain balances, update balance and event together.
1from dataclasses import dataclass
2
3@dataclass(frozen=True)
4class Event:
5 idempotency_key: str
6 account: str
7 delta: int
8 balance_after: int
9
10class Ledger:
11 def __init__(self) -> None:
12 self.balance: dict[str, int] = {}
13 self.events: list[Event] = []
14 self.results: dict[str, Event] = {}
15
16 def apply(self, key: str, account: str, delta: int) -> int:
17 if key in self.results:
18 prior = self.results[key]
19 if (prior.account, prior.delta) != (account, delta):
20 raise ValueError("idempotency key reused with different operation")
21 return prior.balance_after
22 new_balance = self.balance.get(account, 0) + delta
23 if new_balance < 0:
24 raise ValueError("insufficient funds")
25 self.balance[account] = new_balance
26 event = Event(key, account, delta, new_balance)
27 self.events.append(event)
28 self.results[key] = event
29 return new_balance
30
31ledger = Ledger()
32print(ledger.apply("deposit-1", "acct", 100))
33print(ledger.apply("withdraw-1", "acct", -30))
34print(ledger.apply("deposit-1", "acct", 100))
35print(ledger.balance["acct"], len(ledger.events))
36try:
37 ledger.apply("deposit-1", "acct", 200)
38except ValueError as error:
39 print(error)1100
270
3100
470 2
5idempotency key reused with different operationA replay with the same key and operation returns the stored result, even if the account changed later. Reusing a key for a different operation fails loudly instead of silently returning an unrelated balance.
Transfer follow-up:
A reusable lock-order helper makes deadlock prevention concrete:
1from contextlib import ExitStack
2from threading import Lock
3
4locks = {"acct-a": Lock(), "acct-b": Lock()}
5
6def lock_accounts(*account_ids: str) -> ExitStack:
7 stack = ExitStack()
8 for account_id in sorted(set(account_ids)):
9 stack.enter_context(locks[account_id])
10 return stack
11
12with lock_accounts("acct-b", "acct-a"):
13 print("locked:", sorted({"acct-b", "acct-a"}))1locked: ['acct-a', 'acct-b']Log parsers test string handling and explicit malformed-input policy. Group indented continuation lines under the previous valid event. Ignore orphan continuations and malformed records in this base version; a production parser should count or collect rejects.
1import json
2
3def parse_events(text: str) -> list[dict[str, str]]:
4 events: list[dict[str, str]] = []
5 current: dict[str, str] | None = None
6
7 for line in text.splitlines():
8 if line.startswith(" "):
9 if current is not None:
10 current["message"] += f" {line.strip()}"
11 continue
12
13 parts = line.split("|", maxsplit=2)
14 if len(parts) != 3:
15 current = None
16 continue
17
18 timestamp, level, message = parts
19 current = {"timestamp": timestamp, "level": level, "message": message}
20 events.append(current)
21
22 return events
23
24LOGS = """ orphan continuation
252026-06-02T12:00:00Z|ERROR|request failed
26 timeout while calling model
27malformed line
282026-06-02T12:01:00Z|INFO|retry queued"""
29
30print(json.dumps(parse_events(LOGS), indent=2))1[
2 {
3 "timestamp": "2026-06-02T12:00:00Z",
4 "level": "ERROR",
5 "message": "request failed timeout while calling model"
6 },
7 {
8 "timestamp": "2026-06-02T12:01:00Z",
9 "level": "INFO",
10 "message": "retry queued"
11 }
12]When asked to make a solution concurrent, say this before writing code:
X.X.This sounds mechanical because it should. Concurrency interview failures usually come from vague ownership.
Answer every question, then check your score. Score 75% or higher to mark this lesson complete.
9 questions remaining.
Questions and insights from fellow learners.