Futures give fine-grained control: per-task results, timeouts, progress reporting and error handling.
executor.submit(fn, *args, **kwargs) schedules one call and returns a Future, a handle to a result that will exist later.
fut.result(timeout=None)waits and returns the value, or re-raises the exception from the call.fut.exception()returns the exception instead of raising.fut.done(),fut.running(),fut.cancel()(only succeeds if the task hasn't started),fut.add_done_callback(fn).
Process results as they complete, not in submission order, with as_completed:
futures = {pool.submit(fetch, u): u for u in urls}
for fut in as_completed(futures):
url = futures[fut]
try:
data = fut.result()
except Exception as e:
log.warning("%s failed: %s", url, e)The dict maps each future back to its input, a common idiom.
concurrent.futures.wait(fs, timeout, return_when=FIRST_COMPLETED | FIRST_EXCEPTION | ALL_COMPLETED) returns (done, not_done) sets.
executor.shutdown(wait=True, cancel_futures=False) is what the with block calls on exit. Pass cancel_futures=True (3.9+) to drop queued tasks, for example after a fatal error or Ctrl+C. Running tasks can't be interrupted; they must finish on their own.
Example
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
def work(n):
time.sleep(0.1 * (4 - n)) # later items finish first
if n == 2:
raise RuntimeError("task 2 broke")
return n * 10
with ThreadPoolExecutor(max_workers=4) as pool:
futures = {pool.submit(work, n): n for n in range(4)}
for fut in as_completed(futures):
n = futures[fut]
try:
print(f"task {n} -> {fut.result()}")
except RuntimeError as e:
print(f"task {n} failed: {e}")
# task 3 -> 30
# task 2 failed: task 2 broke
# task 1 -> 10
# task 0 -> 0Remember
- submit() returns a Future; result() returns or re-raises
- as_completed yields futures in completion order
- shutdown(cancel_futures=True) drops queued tasks
Watch out for
- Never calling result() on submitted futures, so their exceptions are silently lost.