ThreadPoolExecutor and ProcessPoolExecutor
coreintermediateconcurrent.futures gives threading and multiprocessing the same interface: submit(fn, *args) or map(fn, items). Swap the executor class and the code barely changes — but which one helps depends on I/O-bound vs. CPU-bound.
Think of it as
It's the same job-board pattern either way: you post work (submit) and get back a claim ticket (a Future) to redeem later. ThreadPoolExecutor staffs the board with threads sharing one kitchen (good when workers spend most of their time waiting on something external). ProcessPoolExecutor staffs it with separate kitchens entirely (good when workers are actually busy computing, not waiting).
What we're doing: Submit one task with ThreadPoolExecutor and observe the Future is not done immediately, then run the same CPU-bound function twice through ProcessPoolExecutor and confirm both results come back correct.
- 12
- submit() hands the work to a background thread and returns right away — the print on the next line runs before io_task has slept its 0.1s, so done() is reliably False here.
- 22
- Both cpu_task(N) calls genuinely run in parallel, in separate processes — this is real CPU parallelism, unlike the same code under ThreadPoolExecutor.
- 23
- Calling .result() on each Future in turn blocks until that specific one finishes — collecting all of them waits for the slowest.
future.done() immediately after submit: False
result: task-A done
ProcessPoolExecutor 2 tasks: 0.378s, results: [8999995500000500000, 8999995500000500000]Why this works: submit() is asynchronous by design — it queues the call and returns a Future you can check or block on later, which is exactly why done() reads False right after submitting a task that sleeps for 0.1 seconds. The ProcessPoolExecutor run shows both calls landing on the identical, correct total — confirming the parallel computation did not corrupt or duplicate any work, just distributed it across two real processes.
Iterating executor.map() results discards each one's individual error until you reach it
Wrong
Better
What you see: A single failing item inside executor.map() raises when the loop reaches it, after already silently discarding the chance to see or handle the other results cleanly around it.
Why: executor.map() re-raises an exception from any call the moment its result is consumed, in submission order — it does not give you a way to catch one failure and keep going within the same loop. Submitting with .submit() and handling each Future's .result() individually restores that control, at the cost of writing the loop yourself.
- ex.submit(fn, args) — returns instantly
- Future (pending) — work runs in the background
- future.result() — blocks here until it is actually done
Choosing an executor
Together
Remember: Same API for both — ThreadPoolExecutor for I/O-bound work, ProcessPoolExecutor for CPU-bound. The executor class decides whether you get real parallelism.
See also: cpu bound vs io bound workloads · futures and task lifecycle · multiprocessing basics

