concurrent.futures
concurrent.futures provides high-level Executor pools with Future objects for async results. ThreadPoolExecutor overlaps I/O; ProcessPoolExecutor parallelizes CPU work.
Recipe
from concurrent.futures import ThreadPoolExecutor, as_completed
def task(n: int) -> int:
return n + 1
with ThreadPoolExecutor(max_workers=4) as ex:
futures = [ex.submit(task, i) for i in range(10)]
for fut in as_completed(futures):
print(fut.result())When to reach for this:
- Fan-out blocking calls (HTTP, DB, files)
- CPU pools without manual Process management
mapwith timeout and chunking- Cancelling groups of work on shutdown
- Integration with
asyncio.wrap_future
Working Example
import time
from concurrent.futures import ProcessPoolExecutor, wait, FIRST_COMPLETED
def slow_square(n: int) -> int:
time.sleep(0.05)
return n * n
if __name__ == "__main__":
with ProcessPoolExecutor(max_workers=2) as ex:
futures = [ex.submit(slow_square, i) for i in range(6)]
done, not_done = wait(futures, return_when=FIRST_COMPLETED)
print("first", done.pop().result())
for fut in not_done:
fut.cancel()What this demonstrates:
waitwithFIRST_COMPLETEDfor partial resultscancelbest-effort on not-yet-running tasks- Process pool needs main guard and picklable targets
- Context manager waits for running tasks on exit (unless cancel)
Deep Dive
API Surface
| Call | Behavior |
|---|---|
submit(fn, *args) | Single Future |
map(fn, iterable) | Iterator ordered like input |
as_completed(futures) | Yield as each finishes |
future.result(timeout=) | Block or raise TimeoutError |
Gotchas
- Blocking
result()without timeout - hangs shutdown. Fix: timeouts + cancellation policy. - Exception in worker - stored on Future, raised at
result(). Fix: handle or log per future. - Process pool tiny tasks - spawn cost dominates. Fix: batch work items.
- Shared executor global - complicates tests and lifecycle. Fix: inject executor, close on app shutdown.
- Thread pool for CPU - GIL limits speedup. Fix: ProcessPoolExecutor.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| asyncio.gather | Native async I/O | Blocking SDK |
| multiprocessing.Pool | Legacy code | Greenfield prefers futures |
| joblib / dask | Big data parallelism | Simple scripts |
FAQs
Thread vs Process pool?
Threads for I/O blocking; processes for CPU-bound Python functions.
map vs submit?
map preserves order and batches; submit gives fine-grained per-task control.
How do exceptions propagate?
Raised when calling result() on the failed Future.
shutdown wait=False?
Returns immediately; running tasks continue in background - use carefully on exit.
asyncio integration?
asyncio.wrap_future bridges to awaitable in mixed code.
max_workers default?
min(32, cpu_count + 4) for threads - tune per workload.
Can I use initializer?
Yes on both executor types for per-worker setup (DB connections - mind process safety).
Are callbacks thread-safe?
add_done_callback runs in arbitrary thread - keep callbacks short.
How to test?
Inject executor with max_workers=1 for determinism in unit tests.
GIL and ProcessPoolExecutor?
Each process has own GIL - true parallel bytecode execution.
Related
- threading - primitives under the hood
- multiprocessing - lower-level processes
- Sync/Async Bridging - default executor
- Choosing a Concurrency Model - pick pool type
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.