Most Python rounds circle the same loop: the GIL, how you pick between threads, processes, and asyncio, and a couple of data-model traps that separate people who have read the language from people who have shipped it. The questions repeat because the wrong answers are predictable. This list comes from real loop reports and the rubric behind our Python GIL & concurrency notes.
At a glance, what each area tends to probe:
| Area | Typical question | The lesson |
|---|---|---|
| The GIL | "Will threads speed up a CPU-bound loop?" | python-gil-concurrency |
| Picking a tool | "Threads vs multiprocessing vs asyncio, when?" | concurrency-fundamentals |
| asyncio | "Why did one await freeze the whole server?" | python-async |
| Data model | "What does def f(x, acc=[]) do on the second call?" | python-internals |
The GIL question everyone opens with
What is the GIL, and does Python have real threads?
The GIL is a mutex in CPython that lets only one thread execute Python bytecode at a time. Your threads are real OS threads, but the lock serializes their bytecode, so on the default build you get concurrency, not parallelism, for pure Python work.
Will threading speed up a CPU-bound loop?
No. Two threads crunching numbers contend for the one GIL, and you often end up slightly slower from the switching overhead. Threads still help I/O-bound work because the GIL is released around blocking calls (socket, file reads, time.sleep), so another thread runs while one waits.
Trap "the GIL makes shared state thread-safe, so I don't need locks." False. x += 1 is several bytecodes, a thread can be switched out mid-update, and the increment races. The GIL protects interpreter internals like reference counts, not your program's invariants.
Follow-up "How does a thread ever give up the GIL?" Since 3.2 it is a 5 ms switch interval (sys.setswitchinterval), checked at bytecode boundaries. The old "every 100 bytecodes" answer is outdated and interviewers notice.
For senior color, know the free-threaded build (PEP 703): python3.13t shipped experimental in 3.13, and 3.14 made it officially supported but still optional. The default build on every version still has the GIL.
Study Python GIL & concurrency
Threads vs multiprocessing vs asyncio
This is the routing question, and it maps cleanly to the workload.
- I/O-bound, moderate connection count: threads or
ThreadPoolExecutor. - I/O-bound, thousands of mostly-idle connections: asyncio, because a task is far cheaper than an OS thread.
- CPU-bound:
multiprocessingorProcessPoolExecutor, which sidestep the GIL with separate interpreters. The cost is IPC and pickling, and separate address spaces, so nothing is shared by default.
Red flags "asyncio uses multiple cores for speed" (one thread, cooperative), and "multiprocessing shares memory like threads" (it does not, you pickle or use shared memory). Either answer says the candidate has read tutorials but not measured anything.
The generic theory underneath, races, mutexes, and why a shared counter needs a lock even with the GIL, lives in the fundamentals module.
Study Concurrency fundamentals
asyncio blocking traps
Why did one call freeze the entire server?
asyncio runs on a single thread and only switches at await points. Drop a synchronous call into a coroutine and the whole loop stalls until it returns.
async def handler():
data = requests.get(url).json() # WRONG: sync call, blocks the loop
time.sleep(2) # WRONG: freezes every other task too
return data
async def handler():
data = await client.get(url) # RIGHT: an awaitable
meta = await asyncio.to_thread(heavy_sync_fn) # RIGHT: offload blocking work
return data, meta
asyncio.to_thread (3.9+) or loop.run_in_executor push blocking work off the loop. For CPU-bound work, hand it to a ProcessPoolExecutor instead, because a thread still fights the GIL.
gather vs TaskGroup
Trap on the first failure, gather(*aws) propagates that one exception but leaves the other awaitables running, orphaned. TaskGroup (3.11+) cancels the siblings and raises an ExceptionGroup. The senior answer names structured concurrency and reaches for TaskGroup.
One more that catches people: the loop keeps only weak references to tasks, so a fire-and-forget create_task with no saved reference can be garbage-collected mid-flight. Keep a strong reference in a set and discard it in a done callback, or use a TaskGroup.
Study Python asyncio
Data model traps
The mutable default argument
def append(item, acc=[]): # WRONG: acc is created ONCE, at def time
acc.append(item)
return acc
append(1) # [1]
append(2) # [1, 2] <- same list, not fresh
The default is evaluated once and stored on append.__defaults__, so every call without an argument shares it. Fix with the None sentinel: default to None, then build a new list inside the body.
is vs ==
== compares values, is compares identity (same object). The trap is the interning cache: small ints (-5 to 256) and interned strings make is appear to work on values it should not.
>>> a = 256
>>> a is 256
True # cached small int
>>> b = 257
>>> b is 257
False # separate statements, distinct objects -- use == for value checks
One subtlety worth saying out loud: in a single line or script, CPython folds equal constants into one object, so the semicolon one-liner version prints True. The lesson stands either way: is on numbers is a bug.
Follow-up "Is slicing a copy?" It is a shallow copy. list(x), dict(d), and x[:] copy the top level, but nested mutables stay shared. Reach for copy.deepcopy when the structure is nested.
Late binding in closures belongs to the same cluster: [lambda: i for i in range(3)] returns 2 from all three, because the closure captures the variable, not a snapshot. Bind at def time with lambda i=i: i.
Study Python memory & data model
Where these sit in the loop
Python rarely fills a whole round on its own. The concurrency answers set up system design questions about workers and queues, and the data-model traps sit next to the database and API portions of the same interview. If your loop is backend-heavy, the neighboring rounds are usually SQL, API design, and backend system design.
Red flags across the Python round: claiming threads parallelize CPU work, treating the GIL as a substitute for locks, putting a blocking call in a coroutine and not noticing the loop froze, and "fixing" the mutable default by clearing the list at the top of the function instead of using the sentinel. Any one of them caps the score no matter how fluent the rest of the answer sounds.
The free notes run from the junior floor to the senior internals. Start with the GIL module; the diagnostic will tell you which band you clear today.