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.