concurrent.futures: Executors and Futures
25 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Starting threads by hand and joining them gets long. An executor does that for you and hands back a Future for each job.
Submit a job and get a Future
Future is a placeholder for a result that is not ready yet. result() waits for it and returns the value, or raises the error the job raised.
An executor owns a pool of threads or processes. submit(func, args) hands it a job and returns a Future straight away.
from concurrent.futures import ThreadPoolExecutor
def price_with_tax(amount): return amount * 118 // 100
with ThreadPoolExecutor(max_workers=2) as ex: future = ex.submit(price_with_tax, 1000) print(future.result())future.result() waits for the answer.
Example 1 · submit and result
import timefrom concurrent.futures import ThreadPoolExecutor
def lookup(order_id): time.sleep(0.05) # stands in for a database call return f"order {order_id} shipped"
with ThreadPoolExecutor(max_workers=2) as ex: first = ex.submit(lookup, 101) second = ex.submit(lookup, 102) print(first.result()) print(second.result())order 101 shipped order 102 shipped
What this code does
- Both lookups start at once on two threads.
result()waits for each one. We ask in a fixed order, so the printed order is fixed.
The with block waits for all jobs and then shuts the workers down.
map keeps order, as_completed does not
Executor is an object that runs jobs on a pool of workers. ThreadPoolExecutor uses threads and ProcessPoolExecutor uses processes, with the same methods.
ex.map(func, items) returns results in the order of the items, even if a later job finishes first. as_completed gives futures in finishing order instead.
from concurrent.futures import ThreadPoolExecutor, as_completed
with ThreadPoolExecutor(3) as ex: futures = [ex.submit(len, w) for w in ("ab", "abc", "a")] done = [f.result() for f in as_completed(futures)]print(sorted(done))Sort or index results when you print.
Example 2 · map returns results in input order
import timefrom concurrent.futures import ThreadPoolExecutor
def slow(n): time.sleep((3 - n) * 0.05) # job 1 is the slowest return n * 10
with ThreadPoolExecutor(3) as ex: print(list(ex.map(slow, [1, 2, 3])))[10, 20, 30]
What this code does
- Job 3 finishes first and job 1 last.
mapstill returns[10, 20, 30], because it follows the input order.
Example 3 · as_completed with names
from concurrent.futures import ThreadPoolExecutor, as_completed
def total(prices): return sum(prices)
carts = {"asha": [100, 250], "ben": [40], "chen": [10, 20, 30]}with ThreadPoolExecutor(3) as ex: futures = {ex.submit(total, p): name for name, p in carts.items()} results = {} for f in as_completed(futures): results[futures[f]] = f.result()print(dict(sorted(results.items()))){'asha': 350, 'ben': 40, 'chen': 60}What this code does
- A dict maps each future back to the cart it belongs to.
as_completedgives futures in finishing order, so we store by name and sort at the end.
Use map when you want answers lined up with inputs. Use as_completed when you want to act on each answer as soon as it arrives.
Errors travel with the Future
If a job raises an error, the executor does not crash. The error is stored in the future and raised again when you call result().
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(2) as ex: f = ex.submit(int, "x7") try: f.result() except ValueError: print("bad number")Catch it there, one job at a time.
Example 4 · One bad job does not stop the others
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(2) as ex: futures = {s: ex.submit(int, s) for s in ["12", "x", "7"]}
for text, f in futures.items(): try: print(text, f.result()) except ValueError: print(text, "bad")12 12 x bad 7 7
What this code does
- Each job is its own future. The failing one only affects its own
result(). - Futures are read in dict order, so the output order is fixed.
A job that is never asked for its result() can fail silently. Always read each future you submit.
Threads for waiting, processes for computing
The two executors have the same API, so switching is a one-word change. Pick by the kind of work.
Waiting on a network or disk: threads. Heavy calculation: processes.
Example 5 · The same API with processes
from concurrent.futures import ProcessPoolExecutor
def sum_squares(n): return sum(i * i for i in range(n))
if __name__ == "__main__": with ProcessPoolExecutor(2) as ex: print(list(ex.map(sum_squares, [10, 100, 1000])))[285, 328350, 332833500]
What this code does
- Only the class name changed. The function is at module level, and the start-up code is under the main guard.
- The results come back in input order.
A process pool needs the main guard and picklable functions, as in the multiprocessing lesson. A thread pool needs neither.
Next step
Check your understanding
10 quick questions