Python Module 14 – Concurrency and parallelism: threads, processes, interpreters
concurrent.futures: submit, map, as_completed
Submit calls to an executor, collect results with map() or as_completed(), wait with deadlines, and switch between threads and processes in one line.
What you will learn
- Read the results and errors of calls submitted to an executor through Future objects
- Choose between map(), as_completed() and wait() for the order in which results are needed
- Apply deadlines and cancellation to a batch of calls
- Switch one batch function between threads, processes and interpreters
Before you start
What you’ll learn · Before you start
What you will learn
- Read the results and errors of calls submitted to an executor through Future objects
- Choose between map(), as_completed() and wait() for the order in which results are needed
- Apply deadlines and cancellation to a batch of calls
- Switch one batch function between threads, processes and interpreters
Before you start
On this page
concurrent.futures is one interface for running functions on a pool of workers. An executor takes calls and runs
them on threads, on processes or, from Python 3.14, on interpreters, and for each call it hands you a future: a
handle on a result that may not exist yet. A future can tell you whether its call is done, wait for it with a time
limit, cancel it before it starts, and give you the return value or the exception. Three ways of collecting results
cover most programs. executor.map(fn, items) gives the results in the order of the inputs. executor.submit(fn, *args) returns one future per call, and as_completed(futures) yields those futures in the order they finish.
wait(futures) blocks until all of them are done, until the first one is, or until a deadline. The code that collects
the results stays the same whichever kind of worker runs the calls.
A future, one state at a time
Executors create futures for you. This example makes one by hand, as tests do, and moves it through its states the way a worker would. It runs in your browser, because a future on its own needs no threads:
"""The life of a Future, stepped by hand the way an executor's worker steps it."""
from concurrent.futures import CancelledError, Future
def show(step, future):
print(f"{step:<24} running={future.running()!s:<5} done={future.done()!s:<5} cancelled={future.cancelled()}")
report = Future() # an executor's submit() makes this for you
show("submitted, waiting", report)
report.add_done_callback(lambda f: print(f" callback: the result is {f.result()!r}"))
report.set_running_or_notify_cancel() # a worker picks the task up
show("picked up by a worker", report)
print("cancel() while running:", report.cancel())
report.set_result({"pages": 12}) # the worker stores the return value; callbacks run now
show("finished", report)
print("result():", report.result(), "| exception():", report.exception())
queued = Future()
print("cancel() while waiting:", queued.cancel())
show("cancelled", queued)
try:
queued.result()
except CancelledError:
print("result() raised CancelledError") Output
submitted, waiting running=False done=False cancelled=False
picked up by a worker running=True done=False cancelled=False
cancel() while running: False
callback: the result is {'pages': 12}
finished running=False done=True cancelled=False
result(): {'pages': 12} | exception(): None
cancel() while waiting: True
cancelled running=False done=True cancelled=True
result() raised CancelledError
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 future_states.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The states of a future
Text description of the diagram
The diagram shows four states and the arrows between them.
- Pending: the call waits in the executor's queue. From here a worker can take it, or cancel() can cancel it.
- Running: a worker is executing the call. cancel() returns False now, and the call goes on to the end.
- Finished: the call returned or raised. result() returns the value or raises the same exception, and exception() returns the exception or None.
- Cancelled: the call never ran. result() raises CancelledError.
done() is true in the last two states, Finished and Cancelled.
A future starts pending, becomes running when a worker takes the call, and ends finished, holding either a return
value or an exception. Only a pending future can be cancelled, which is why cancel() returned False once the
worker had started. The callback ran inside set_result(), the moment the result arrived, which is how an executor
notifies code registered with add_done_callback(). result() returns the value of a finished call or raises the
exception the call raised, and exception() returns that exception instead of raising it. In real code you never
call set_result() yourself: submit() creates the future, a worker runs the call, and the executor stores what
happened. Your side only reads futures, waits for them and sometimes cancels them.
map() or as_completed(): the order results arrive in
Four simulated downloads of different lengths run on four threads, collected first with map() and then with
submit() and as_completed():
"""Four downloads of different lengths, collected with map() and with as_completed()."""
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
SECONDS = {"a.csv": 0.8, "b.csv": 0.2, "c.csv": 0.6, "d.csv": 0.4}
def download(name):
time.sleep(SECONDS[name]) # waiting for the network, simulated
return f"{name} ({SECONDS[name]} s)"
with ThreadPoolExecutor(max_workers=4) as pool:
print("map(): results in the order of the inputs")
began = time.perf_counter()
for result in pool.map(download, SECONDS):
print(f" after {time.perf_counter() - began:.1f} s: {result}")
print("as_completed(): results in the order they finish")
began = time.perf_counter()
futures = {pool.submit(download, name): name for name in SECONDS}
for count, future in enumerate(as_completed(futures), start=1):
print(f" after {time.perf_counter() - began:.1f} s: {count}/{len(futures)} {future.result()}") Output
map(): results in the order of the inputs after 0.8 s: a.csv (0.8 s) after 0.8 s: b.csv (0.2 s) after 0.8 s: c.csv (0.6 s) after 0.8 s: d.csv (0.4 s) as_completed(): results in the order they finish after 0.2 s: 1/4 b.csv (0.2 s) after 0.4 s: 2/4 d.csv (0.4 s) after 0.6 s: 3/4 c.csv (0.6 s) after 0.8 s: 4/4 a.csv (0.8 s)
This output changes from run to run: the waits are timed on each run, so a busy computer can add a tenth of a second here and there; the order of the results does not change
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 progress.py
Both batches took 0.8 seconds, the length of the slowest download, because the four calls overlapped. What differs is
when each result became available. map() gave nothing for 0.8 seconds, because its first input was the slowest,
and then all four at once. as_completed() gave b.csv after 0.2 seconds and each later file as it finished. Use
map() when you need every result in input order. Use as_completed() when each result is useful on its own: to show
progress, to start the next step early, or to see a failure as soon as it happens. Errors follow the same split. If
one download raised, map() would raise that exception when the loop reached its place in the input order, and the
loop would end there. as_completed() hands over a failed future like any other, and future.result() raises inside
the loop, where you can catch it for that one file and carry on with the rest.
Which executor runs the calls
The executor decides where the calls run. ThreadPoolExecutor suits calls that wait for the network or the disk.
ProcessPoolExecutor suits pure-Python computation, with the costs measured in the previous lesson: pickled arguments
and a slow start. InterpreterPoolExecutor, new in Python 3.14, gives each worker its own interpreter inside the same
process, so it computes in parallel like processes and starts faster, with limits of its own. Because all three share
the Executor interface, switching between them changes one name.
Four ways to collect results
| Collect with | Results arrive | A call that raised |
|---|---|---|
submit() and result() |
One future per call, read in the order you ask | result() raises its error |
map() |
Results in the order of the inputs | Raises when the loop reaches it |
as_completed() |
Futures in the order the calls finish | Its future holds the error |
wait() |
Two sets, done and not done, when all, the first or the first failure is done | Its future holds the error |
wait() takes a return_when argument. FIRST_COMPLETED returns as soon as any call finishes, which suits taking the
fastest of several answers. FIRST_EXCEPTION returns at the first failure, so a batch can stop early. A timeout ends
the wait, never the work: calls that are running go on. cancel() stops only calls that have not started, and
shutdown(cancel_futures=True) cancels every queued call at once. buffersize, new in Python 3.14, limits how far
map() reads its inputs ahead of the results you take; without it, map() submits a call for every input before you
see the first result.
Check yourself
Four questions on futures
4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Deadlines, first answers and early failures, with each case run Pro: opens with a Pro, Premium or Ultimate pass
Real batches rarely wait for everything. This part runs four of them: a deadline that keeps what is done, a timeout that cancels or does not, the fastest of three replicas, and a batch that stops at its first error.
In this part:
- A deadline for the whole batch that keeps what finished
- What a timeout cancels: map() against as_completed()
- The fastest of three replicas, and a batch that fails early
3 runnable examples (Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
One interface on threads, processes and interpreters, measured Pro: opens with a Pro, Premium or Ultimate pass
The same batch function runs on three kinds of workers when one class name changes. This part times all three,
counts how far map() reads ahead of you, and traces where callbacks run.
In this part:
- Changing one class name: threads, processes and interpreters, timed
- How far map() reads ahead: 50,000 inputs with and without buffersize
- Where callbacks run, and what a failing callback does
- Exercise: run a batch against a deadline
- Run a batch against a deadline and keep what finished
4 runnable examples (Python) · 1 exercise with sample tests · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
When do you collect results with executor.map(), and when with submit() and as_completed()?
executor.map(fn, items) returns the results in the order of the inputs. It suits batches where you need every result and their order matters, such as one output row per input row. Its first result waits for the first input's call, however long that takes, and an exception ends the loop when iteration reaches the failed item. submit() returns one future per call, and as_completed(futures) yields those futures as they finish. It suits work where each result is useful on its own: showing progress, starting the next step for whichever file is ready, or handling each failure where it happens, because future.result() raises inside the loop for that one call. Both take a timeout that ends the wait but never the calls already running.
-
Mid to senior
A checkout page calls five services in parallel on a ThreadPoolExecutor and must answer within 800 ms. Payment and stock are required; offers, delivery estimate and recommendations are optional. Design the waiting, the timeouts and the failure handling.
-
Mid to senior
A batch job saves each result to a database from an add_done_callback() on its future. Some results never reach the database, the job reports success, and the request error metrics show nothing. Explain what happened and redesign it.
-
Mid to staff
A job calls executor.map(parse, open("huge.log")) on a 40 GB log file and runs out of memory before it prints a single result. Explain why, and fix it for Python 3.14 and for Python 3.12.
Key takeaways
submit()returns a future at once;result()waits for the value or raises the call’s exception.map()gives results in input order;as_completed()gives futures in the order they finish.wait()returns when all, the first or the first failing call is done, or when its timeout passes.- A timeout ends the wait, not the work: only calls that have not started can be cancelled.
- The same collecting code works on
ThreadPoolExecutor,ProcessPoolExecutorand, from Python 3.14,InterpreterPoolExecutor.
References
- concurrent.futures: launching parallel tasks (Python Software Foundation)
- concurrent.futures: Future objects (Python Software Foundation)
- concurrent.futures: module functions wait() and as_completed() (Python Software Foundation)
- concurrent.futures: InterpreterPoolExecutor (Python Software Foundation)
- What's New In Python 3.14: concurrent.futures (Python Software Foundation)
- tracemalloc: trace memory allocations (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress