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.