Skip to content

Python · Concurrency & Parallelism

Python concurrent.futures Explained — ThreadPoolExecutor, ProcessPoolExecutor, and Futures

4 min read Updated 2026-06-19 Share:

Practice concurrent.futures interview questions

Python concurrent.futures, explained

concurrent.futures is the high-level way to run code in parallel. Instead of managing threads or processes by hand, you hand work to an executor and get back Future objects. The same tiny API works for both threads and processes, so switching between I/O-bound and CPU-bound strategies is often a one-line change.

The Executor pattern

An executor manages a pool of workers. You submit callables; it runs them on the pool and returns a Future representing the eventual result. Use it as a context manager so the pool is cleaned up automatically.

from concurrent.futures import ThreadPoolExecutor

def square(n):
    return n * n

with ThreadPoolExecutor(max_workers=4) as pool:
    future = pool.submit(square, 5)   # schedules the call, returns immediately
    print(future.result())            # 25 — blocks until done

submit returns instantly; the work happens on a worker thread. Calling .result() blocks until that work finishes (or re-raises any exception it threw).

Threads vs processes — pick by workload

The two executors share an identical interface but solve opposite problems:

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

# I/O-bound (network, disk): threads — GIL is released during I/O waits
with ThreadPoolExecutor(max_workers=20) as pool:
    ...

# CPU-bound (math, parsing): processes — each has its own GIL, true parallelism
with ProcessPoolExecutor(max_workers=4) as pool:
    ...

Threads share memory and are cheap but the GIL serialises CPU work. Processes get real parallelism but pay for pickling arguments/results across the process boundary.

map runs a function over an iterable

executor.map mirrors the built-in map, but parallel. It returns results in input order and is the cleanest option when you apply one function to many inputs.

from concurrent.futures import ProcessPoolExecutor

def heavy(n):
    return sum(i * i for i in range(n))

with ProcessPoolExecutor() as pool:
    for result in pool.map(heavy, [100_000, 200_000, 300_000]):
        print(result)        # yielded in the order of the inputs

map re-raises the first exception when you reach that result during iteration. For more control over completion order or errors, use submit instead.

Collecting results as they finish

With submit you hold the futures and can react the moment each completes — useful when some tasks are much slower than others. as_completed yields futures in finish order.

from concurrent.futures import ThreadPoolExecutor, as_completed

urls = ["a", "b", "c"]

with ThreadPoolExecutor() as pool:
    futures = {pool.submit(fetch, url): url for url in urls}
    for fut in as_completed(futures):
        url = futures[fut]
        print(url, fut.result())     # printed in whatever order finishes first

The dict maps each future back to its input so you know which result is which.

What a Future gives you

A Future is a handle to a result that may not exist yet. Beyond .result(timeout=...) you can inspect state and attach callbacks:

fut = pool.submit(heavy, 1_000_000)
fut.done()                       # False while running
fut.add_done_callback(lambda f: print("finished:", f.result()))
fut.result(timeout=5)            # raises TimeoutError if not ready in 5s

If the callable raised, .result() re-raises that exception in the calling thread, and .exception() returns it without raising — so errors are never silently lost.

Handling exceptions cleanly

Exceptions from workers surface only when you touch the result, so always read it:

with ThreadPoolExecutor() as pool:
    futures = [pool.submit(risky, x) for x in data]
    for fut in as_completed(futures):
        try:
            fut.result()
        except ValueError as e:
            print("task failed:", e)   # one bad task doesn't kill the rest

Recap

concurrent.futures gives you one high-level API over two backends: ThreadPoolExecutor for I/O-bound work (cheap, shared memory, GIL released on I/O) and ProcessPoolExecutor for CPU-bound work (true parallelism, but arguments are pickled). Use map for ordered results of one function over many inputs, and submit + as_completed when you want results as they finish or finer error control. A Future is a placeholder whose .result() blocks until ready and re-raises any worker exception, so always consume it.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel