Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 24 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 9 of 24 minutes free
Free: A future, one state at a time · map() or ascompleted(): the order results arrive in · Which executor runs the calls · 2 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 15 minutes.

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:

One future, from submitted to finished, and one cancelled Python · future_states.py
"""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

A future goes from pending to running to finished; only a pending future can move to cancelled.Pendingwaiting in theexecutor's queueRunningin a workerFinishedresult() returns the valueor raises the call's errorCancelledresult() raisesCancelledErrora worker takes itthe call returns or raisescancel() succeeds

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():

The same four downloads, collected two ways Python · progress.py
"""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.

  1. Question 1 of 4 What does this print, and when?

    Read the code, then choose one answer.

    import time
    from concurrent.futures import ThreadPoolExecutor
    
    def nap(seconds):
        time.sleep(seconds)
        return seconds
    
    with ThreadPoolExecutor(max_workers=3) as pool:
        print(list(pool.map(nap, [0.3, 0.1, 0.2])))
    Show the answer to question 1

    Answer: [0.3, 0.1, 0.2], after about 0.3 seconds

    map() returns results in the order of the inputs, whatever order the calls finish in. The three calls run at the same time on three threads, so the whole list is ready when the slowest one, 0.3 seconds, is done.

  2. Question 2 of 4 future.cancel() returned False. What does that tell you?

    Choose one answer.

    Show the answer to question 2

    Answer: The call had already started or finished, so it was not cancelled and runs to its end

    Only a call that is still waiting in the executor's queue can be cancelled. Once a worker has started it, Python cannot stop it from outside, so cancel() returns False and the call goes on.

  3. Question 3 of 4 A dashboard fetches data for 30 tiles from 30 services and must draw each tile as soon as its data arrives. Which way of collecting the results fits?

    Choose one answer.

    Show the answer to question 3

    Answer: submit() for each service, then as_completed() over the futures

    as_completed() yields each future as soon as it finishes, so a fast service's tile is drawn at once. map() and calling result() in submission order both make every tile wait for the slower services before it.

  4. Question 4 of 4 as_completed(futures, timeout=2) raised TimeoutError. What happened to the calls that were still running?

    Choose one answer.

    Show the answer to question 4

    Answer: They are still running: the timeout ended the wait, not the calls

    A timeout limits how long your code waits. The calls themselves go on in their workers, and their results arrive in the futures later, whether or not anyone reads them.

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.

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.

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

  1. 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.

  2. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. 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.

    Model answer, Pro. First step and follow-ups

  4. 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.

    Model answer, Pro. First step and follow-ups

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, ProcessPoolExecutor and, from Python 3.14, InterpreterPoolExecutor.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.