concurrent.futures Interview Questions & Answers
6 questions Updated 2026-06-18
Python interview questions on concurrent.futures: the Executor abstraction, ThreadPoolExecutor vs ProcessPoolExecutor, submit and Future objects, map, as_completed, and exception handling.
concurrent.futures provides a high-level, uniform interface for running
callables asynchronously. An Executor manages a pool of workers and
hands you back Future objects representing pending results — and the same
API works whether the workers are threads or processes, so you can swap one for
the other with a one-line change.
from concurrent.futures import ThreadPoolExecutor
def work(x):
return x * 2
with ThreadPoolExecutor(max_workers=4) as ex: # context manager auto-shuts down
future = ex.submit(work, 10) # schedule the call
print(future.result()) # 20
# swap to processes by changing only the class name:
# with ProcessPoolExecutor() as ex: ...
The context manager (with) cleanly handles worker shutdown and waits for
pending work on exit. Rule of thumb: prefer concurrent.futures over raw
threading/multiprocessing when you just want to run a function over a pool
and collect results.
Both share the Executor API but differ in workers. ThreadPoolExecutor runs
tasks in threads within one process — cheap, shared memory, but bound by the
GIL, so it only helps I/O-bound work. ProcessPoolExecutor runs tasks
in separate processes, each with its own GIL, giving true parallelism for
CPU-bound work (at the cost of pickling arguments/results).
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
# I/O-bound: many network/disk waits -> threads
with ThreadPoolExecutor() as ex:
ex.map(download, urls)
# CPU-bound: number crunching -> processes
with ProcessPoolExecutor() as ex:
ex.map(crunch, datasets)
Because the API is identical, you can prototype with threads and switch to
processes if the GIL becomes the bottleneck. Rule of thumb: I/O-bound ->
ThreadPoolExecutor; CPU-bound -> ProcessPoolExecutor.
submit(fn, *args) schedules fn to run in the pool and immediately
returns a Future — a handle to a result that may not exist yet. The Future
lets you check status (done(), running()), block for the result
(result()), retrieve an exception (exception()), cancel(), or attach a
callback (add_done_callback).
from concurrent.futures import ThreadPoolExecutor
def slow_double(x):
return x * 2
with ThreadPoolExecutor() as ex:
fut = ex.submit(slow_double, 21) # returns instantly
print(fut.done()) # False — probably still running
print(fut.result()) # 42 — blocks until ready
result(timeout=...) blocks (up to an optional timeout) until the value is
ready. A Future decouples starting the work from collecting it, which is
what makes overlapping multiple calls possible.
executor.map(fn, iterable) is the convenient bulk form: it applies fn to
every item concurrently and returns an iterator of results in input order —
analogous to the built-in map, but parallel. submit is lower level,
giving you a Future per call for fine-grained control.
from concurrent.futures import ThreadPoolExecutor
def fetch(url):
return len(url)
urls = ["a", "bb", "ccc"]
with ThreadPoolExecutor() as ex:
for result in ex.map(fetch, urls): # results stream back in order
print(result) # 1, 2, 3
map is great when you have a clean iterable and want ordered results with
minimal code. Use submit (often with as_completed) when you need results
as they finish, per-task error handling, or cancellation. Note map raises
the first exception when you iterate to that result.
as_completed(futures) yields each Future as soon as it finishes,
regardless of submission order — so you can process results the moment they're
ready instead of waiting for the slowest task to keep its place (as map's
ordered output would).
from concurrent.futures import ThreadPoolExecutor, as_completed
def work(n):
return n * n
with ThreadPoolExecutor() as ex:
futures = [ex.submit(work, i) for i in range(5)]
for fut in as_completed(futures): # whichever finishes first
print(fut.result()) # order is non-deterministic
This is ideal for responsiveness — show progress as tasks complete, or
handle failures immediately. Use map when you want results in input order; use
as_completed when you want them in completion order.
An exception raised inside a worker is captured and stored in its Future,
not raised at submit time. It re-raises when you call future.result() (or
iterate to that item in map). You can also inspect it without raising via
future.exception().
from concurrent.futures import ThreadPoolExecutor
def boom(x):
raise ValueError(f"bad: {x}")
with ThreadPoolExecutor() as ex:
fut = ex.submit(boom, 1)
err = fut.exception() # returns the ValueError, doesn't raise
print(err) # bad: 1
fut.result() # NOW it re-raises ValueError
The implication: if you never call result() (or check exception()), an
error can pass silently. Always retrieve results — typically in a try/except
around result() — so worker failures surface in the main thread.
Practice tests are coming soon
Get notified when interactive mock interviews and quizzes launch.