Python Module 14
Concurrency and parallelism: threads, processes, interpreters interview questions
Interview questions on “Concurrency and parallelism: threads, processes, interpreters”.
Warm-up questions
Short questions with short answers, each answered in full.
-
Fresher to mid
What is the difference between concurrency and parallelism, and which of the two does the GIL take away from threads?
Answered in the lesson Concurrency models and the GIL explained
-
Fresher to mid
What do start() and join() do on a threading.Thread, and what happens to a daemon thread when the program exits?
Answered in the lesson Threads: threading and ThreadPoolExecutor
-
Fresher to mid
What is the difference between threading.Lock and threading.RLock, and when do you need an RLock?
Answered in the lesson Thread safety: locks, events and queues
-
Fresher to mid
Why should you pass a list of arguments to subprocess.run() instead of one command string?
Answered in the lesson Running other programs with subprocess
-
Fresher to mid
What does if __name__ == "__main__" do in a script that uses multiprocessing, and why do Windows and macOS need it?
Answered in the lesson Processes with multiprocessing and pools
-
Fresher to mid
When do you collect results with executor.map(), and when with submit() and as_completed()?
Answered in the lesson concurrent.futures: submit, map, as_completed
-
Mid to senior
What is the difference between sys._is_gil_enabled() and the Py_GIL_DISABLED configuration variable, and when do you use each?
Answered in the lesson Running Python without the GIL (3.14t builds)
-
Mid to senior
What does it mean that interpreters created with concurrent.interpreters are isolated, and what is it not?
Answered in the lesson Subinterpreters with concurrent.interpreters
Questions with model answers
What each question tests, the first step of a good answer and the follow-ups to expect.
-
Mid to senior
A nightly report job makes 400 HTTP calls of about 0.25 s each, one after another, then spends 30 s computing statistics in pure Python. A teammate proposes "just use threads". Estimate what threads would save, and say what you would do with each phase.
- What it tests
- Splitting a job into I/O-bound and CPU-bound phases, estimating each with numbers, and matching each phase to a model instead of applying one fix everywhere.
- First step
- Split the job into its two phases and estimate each. The calls take 400 × 0.25 s = 100 s in turn, and their time is almost all waiting, so threads overlap it. The 30 s of pure-Python computing holds the GIL, so threads on the default build cannot shorten it.
- Follow-ups
-
- How many threads would you use for the HTTP phase, and what limits that number?
- How would you speed up the 30-second phase?
- The server allows only 10 requests per second. What changes?
From the lesson Concurrency models and the GIL explained
Model answer, Pro. Model answers open with any pass; prices are in the notice on this page.Opening the model answers with your pass…
Model answers open with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so the model answers can’t be opened at the moment. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Senior to staff
Your team wants to move a CPU-heavy Python service to the free-threaded build of Python 3.14 so that its worker threads run in parallel. What do you check before switching, and how do you show that the switch helped?
- What it tests
- What the free-threaded build changes and what it does not, extension compatibility, thread safety of the service's own code, and measuring speed and memory before deciding.
- First step
- Separate the promise from the preconditions. Threads run Python code in parallel only if the GIL really stays off, so first check every extension module the service imports, because one that is not marked as free-threading safe turns the GIL back on. Then plan a measurement on the real workload, not a micro-benchmark.
- Follow-ups
-
- How do you confirm in production that the GIL is really off?
- Which bugs can appear that never showed up with the GIL?
- When would you stay on the default build and use processes instead?
From the lesson Concurrency models and the GIL explained
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
Every two minutes a job must check the status of up to 6,000 pending UPI payments through a provider's API. Each call takes about 400 ms, and the provider allows at most 25 calls in flight. Design the threading part: how many threads, and how do you handle slow and failed calls?
- What it tests
- Sizing a thread pool from a deadline, a latency and an external limit, and handling timeouts and failures per item without losing the rest of the batch.
- First step
- Work out whether the deadline can be met at all. In turn, 6,000 × 0.4 s is 2,400 s, twenty times the two-minute window. With 25 calls in flight, the batch takes 6,000 ÷ 25 × 0.4 s = 96 s, which fits inside it.
- Follow-ups
-
- What happens if you use 100 threads instead of 25?
- A few calls hang for 30 seconds. How do you stop them from delaying the batch?
- How do you make sure no payment is checked twice or skipped when a run overlaps the next one?
From the lesson Threads: threading and ThreadPoolExecutor
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A background thread that uploads log files stopped working a week ago, and nobody noticed: the service kept running and no error reached the monitoring. Explain how that can happen, and how you would make such failures visible.
- What it tests
- Where an exception that ends a thread goes, threading.excepthook, futures as the carrier of errors, and health checks for background work.
- First step
- Start with where the error went: an exception that ends a thread never travels to the thread that started it, so nothing interrupted the service. Then ask where the thread's error report was written, and whether anyone reads that place.
- Follow-ups
-
- How would you write a custom excepthook, and what should it do?
- Why do futures from a ThreadPoolExecutor not lose the error in the same way?
- How do you detect a worker that is alive but stuck rather than dead?
From the lesson Threads: threading and ThreadPoolExecutor
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Senior to staff
A counter of processed orders is updated from a pool of threads. Every test passes, but in production the daily total is a few hundred short of the database's count. How do you prove that a race causes it, and how do you fix it for good?
- What it tests
- Recognising a lost update, reproducing a race deterministically instead of hoping, and choosing between a lock, a single owner of the data and returning results instead of sharing state.
- First step
- Look for a read-modify-write on shared data, such as total = total + n or a dictionary entry updated in place, and for anything between the read and the write that can wait, such as a log call or a database query. That gap is where another thread reads the same old value.
- Follow-ups
-
- Why did the tests never catch it?
- Lock, queue or no shared state at all: which would you choose here, and why?
- What changes on the free-threaded build?
From the lesson Threads: threading and ThreadPoolExecutor
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A booking service runs on a thread pool. Its code checks
if seats_left > 0, charges the card, then decrementsseats_left. During a burst it sold 3 more tickets than the train had seats. Explain the bug, fix it, and say what changes when the count lives in a database.- What it tests
- Recognising check-then-act, making the check and the update one atomic step, keeping slow work outside the lock, and carrying the same idea to a database.
- First step
- Point at the gap: several threads can pass the check before any of them decrements, because the card charge in between takes time. The check and the decrement have to become one step that no other thread can interrupt.
- Follow-ups
-
- Why not simply hold the lock during the card charge?
- How do you test the fix so that it would have failed before?
- The service now runs on four machines. Does the lock still help?
From the lesson Thread safety: locks, events and queues
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Senior to staff
A service that uses two locks freezes about once a week under load: no CPU use, no errors, requests just stop. How do you confirm that it is a deadlock, find the two code paths involved, and prevent it from coming back?
- What it tests
- Recognising the signs of a deadlock, collecting every thread's stack from a live process, lock ordering, and guards that make a regression fail in tests.
- First step
- Collect evidence before restarting: a frozen process that uses no CPU is waiting for something, so record what every thread is waiting for at that moment, then compare those places with the locks each thread holds.
- Follow-ups
-
- How do you get every thread's stack from a process that no longer responds?
- Why does it happen only once a week?
- Would acquire(timeout=...) fix it?
From the lesson Thread safety: locks, events and queues
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
Design a pipeline that resizes 80,000 product photos of about 3 MB each: one thread reads files, three threads resize, one thread writes the results. How do you bound memory, report failed photos, and stop cleanly on Ctrl+C?
- What it tests
- Producer-consumer design with bounded queues, sizing a queue from memory, per-item error handling, and an orderly shutdown that works with signals arriving in the main thread.
- First step
- Put a bounded queue between each pair of stages and size it from memory. Estimate the worst case first: if the reader can outrun the resizers, an unbounded queue could end up holding every photo it has read, so ask how many photos may wait in memory at once and turn that into a maxsize.
- Follow-ups
-
- Why are threads enough here, and when would you switch to processes?
- One photo is corrupt and makes the resizer raise. What should happen?
- How does the main thread learn about Ctrl+C, and what does it do next?
From the lesson Thread safety: locks, events and queues
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Fresher to senior
A web endpoint runs
subprocess.run(f"convert {filename} out.png", shell=True), wherefilenamecomes from the upload form. What is wrong with it, and how do you rewrite it so that no file name can change the command?- What it tests
- Recognising that shell=True with outside input hands that input to a shell, and replacing it with an argument list, validation and the other defences a server needs.
- First step
- Say what the shell does with the string: it reads the whole line as a command, so characters in the file name that the shell treats as syntax change what runs. The fix starts with removing the shell: pass a list, with the file name as one item, so no shell ever reads it.
- Follow-ups
-
- Is shlex.quote() a good enough fix?
- The file name starts with a hyphen. Is the list version still safe?
- What else would you check before this endpoint runs a program at all?
From the lesson Running other programs with subprocess
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A nightly job runs a backup tool with subprocess.Popen(..., stdout=PIPE, stderr=PIPE) and then calls wait(). Most nights it works; on nights with large backups it never finishes. What is happening, and how do you fix it?
- What it tests
- Pipe buffers and the deadlock between a writer and a waiting parent, communicate() and run(), timeouts that kill, and turning a hang into a reported failure.
- First step
- Notice that nobody reads the pipes while the parent waits. A pipe holds only a limited amount of data, so a large backup that prints a lot can fill it and block, while the parent waits for the tool to exit.
- Follow-ups
-
- Why does it work on small nights?
- How would you add a time limit without leaving the tool running?
- The tool's output is gigabytes of progress lines. What do you do with it?
From the lesson Running other programs with subprocess
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A script runs
git statuswith subprocess, splits the output into lines and parses them to find changed files. On some machines it crashes with UnicodeDecodeError, and on others it misses files. Why, and how do you make it reliable?- What it tests
- Decoding a child's output, locale and translated messages, file names with unusual characters, and preferring a machine-readable output format with explicit decoding.
- First step
- Separate the two failures. The crash comes from decoding: text=True uses the machine's locale encoding, and one byte in a file name that is not valid in it raises UnicodeDecodeError. The missed files come from parsing output meant for people, whose wording, translation and quoting differ between machines and versions.
- Follow-ups
-
- Which git options make the output safe to parse?
- How do you read file names that are not valid UTF-8?
- What do you check about the exit status before you parse anything?
From the lesson Running other programs with subprocess
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A teammate moved a loop over 200,000 small records to ProcessPoolExecutor.map() and the job became ten times slower than the plain loop. What most likely went wrong, how would you measure it, and what would you change?
- What it tests
- Per-task messaging and pickling costs, start-up cost, choosing a chunk size, and measuring before parallelising.
- First step
- Compare the work per record with the cost of one round trip. ProcessPoolExecutor.map() sends one item per message by default, so 200,000 records mean 200,000 pickled tasks and results, all handled by the main process. If a record takes microseconds, the messaging dominates; time the plain loop per record, then the pool with a large chunksize.
- Follow-ups
-
- What chunksize would you start with, and why?
- The records are large dictionaries. What changes?
- When would you keep the plain loop?
From the lesson Processes with multiprocessing and pools
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to staff
A Linux service ran fine on Python 3.13. On 3.14 its workers see empty configuration, and a lambda passed as the pool's initializer raises PicklingError. What changed, and how do you fix it without forcing the old start method?
- What it tests
- The 3.14 change of the default start method on Linux, what fork let code get away with, and passing state to workers explicitly.
- First step
- Name the change: from Python 3.14 the default start method on Linux is forkserver, not fork. A forked worker was a copy of the parent, so globals filled at run time were there and its initializer was never pickled; a forkserver worker gets its state by importing the script and receives the initializer and its arguments pickled.
- Follow-ups
-
- Why not set the start method back to fork?
- How do you give every worker a large configuration once?
- How could tests have caught this before the upgrade?
From the lesson Processes with multiprocessing and pools
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A checkout page calls five services in parallel on a ThreadPoolExecutor and must answer within 800 ms. Payment and stock are required; offers, delivery estimate and recommendations are optional. Design the waiting, the timeouts and the failure handling.
- What it tests
- Deadlines across a batch with wait(), required against optional calls, what timeouts do to running calls, and keeping abandoned work from exhausting the pool.
- First step
- Separate the deadline for the page from the timeouts of each call. Submit all five, then wait(futures, timeout=budget) once, where budget is what is left of the 800 ms. After it returns, a missing required result fails the request, and a missing optional one is replaced by a default.
- Follow-ups
-
- The payment call itself can take 2 seconds. What do you do?
- What happens to the pool when the optional calls keep timing out?
- How do you test the deadline without waiting 800 ms in every test?
From the lesson concurrent.futures: submit, map, as_completed
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A batch job saves each result to a database from an add_done_callback() on its future. Some results never reach the database, the job reports success, and the request error metrics show nothing. Explain what happened and redesign it.
- What it tests
- Where done callbacks run, what happens to an exception raised in one, and collecting results so that every failure reaches the caller.
- First step
- Find out which thread runs the callback and what happens to an exception raised inside it. Then follow one failed database write from the callback to the job's exit status, and see where the error stops travelling.
- Follow-ups
-
- Why did the job still report success?
- Where should the database write happen instead?
- How would a test have caught it?
From the lesson concurrent.futures: submit, map, as_completed
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to staff
A job calls executor.map(parse, open("huge.log")) on a 40 GB log file and runs out of memory before it prints a single result. Explain why, and fix it for Python 3.14 and for Python 3.12.
- What it tests
- How Executor.map() submits its inputs, estimating the memory of pending futures, buffersize in 3.14, and a bounded submit loop for older versions.
- First step
- Read what map() does with its input: before it returns the first result it collects the whole iterable and submits one call per item, so every line of the file becomes a pending future holding the line. Estimate the number of lines, multiply by the size of a line plus a future, and compare it with the memory available.
- Follow-ups
-
- Estimate the memory for 400 million lines.
- How do you keep results in input order with a bounded loop?
- Why not read the file in a worker instead?
From the lesson concurrent.futures: submit, map, as_completed
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Senior to staff
A service was moved to python3.14t so that its 16 worker threads compute in parallel, but its CPU use never goes above one core, and a warning about the GIL appears once in the start-up logs. Diagnose it and describe the fix.
- What it tests
- How an extension module turns the GIL back on, confirming it from inside the process, and the options from upgrading to isolating the module.
- First step
- Confirm the state before guessing: log sys._is_gil_enabled() after start-up. If it is True on a free-threaded build, something turned the GIL on, and the start-up warning names the culprit: an extension module that has not declared it can run without the GIL, which makes Python enable the GIL for the whole process as it loads.
- Follow-ups
-
- The module is a small internal C extension. What has to change in it?
- Would PYTHON_GIL=0 be an acceptable fix?
- How do you stop this from reaching production again?
From the lesson Running Python without the GIL (3.14t builds)
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Senior to staff
After switching a billing service to python3.14t, invoice totals computed in worker threads round differently, although no code changed. The threads are started inside a decimal.localcontext() block. What changed, and how do you make the service behave the same on both builds?
- What it tests
- The thread_inherit_context default of the free-threaded build, context variables in threads, and making behaviour explicit instead of relying on build defaults.
- First step
- Name the default that differs: on the free-threaded build, the flag thread_inherit_context is on, so a new thread starts with a copy of its creator's context, decimal precision and rounding included. On the default build it is off and threads start with an empty context, the standard 28 digits, which is what the service's totals had silently relied on.
- Follow-ups
-
- Which other behaviour changes with the same kind of default?
- Which fix would you choose, the flag or the code?
- How would a test have caught this before the switch?
From the lesson Running Python without the GIL (3.14t builds)
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A service scores incoming documents with a pure-Python model that takes about 400 ms of CPU per document, using a ProcessPoolExecutor of 8 workers. A colleague proposes InterpreterPoolExecutor instead. How do you decide, and what would you measure first?
- What it tests
- The trade-offs between processes and interpreters: start-up, memory, data passing and extension support, decided with measurements rather than preference.
- First step
- List what the workers import and test each import in a fresh interpreter first: one dependency that refuses to load in a subinterpreter ends the discussion. Then compare what interpreters would save, start-up time and memory per worker, with what the job spends, 400 ms of CPU per document.
- Follow-ups
-
- The model is a 300 MB dictionary every worker needs. What changes?
- How do you roll it out safely?
- What would make you stay with processes?
From the lesson Subinterpreters with concurrent.interpreters
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
After moving a batch job to InterpreterPoolExecutor, every task fails with an ImportError saying a module "does not support loading in subinterpreters", although the same import works in the main interpreter. Explain why, and what your options are.
- What it tests
- Single-phase and multi-phase extension modules, why isolation needs module support, and choosing a fallback.
- First step
- Read the error as a property of the extension module rather than of your code. Find out which module raised it, which of your imports pulls it in, and whether a newer release of it supports multiple interpreters, before you change any of the job's own code.
- Follow-ups
-
- Can you load the module only in the main interpreter and still use interpreters?
- How do you find every such module before switching?
- What would the library's authors have to change?
From the lesson Subinterpreters with concurrent.interpreters
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes