← Practical Python
10 / 10 · 50 MIN

Concurrency, deadlines, and task recovery

Coordinate futures without losing identity, bound waiting work, and distinguish timeout from cancellation.

Execution state and accepted outcome

A Future represents a scheduled call; receiving it does not establish that work finished. done includes normal completion, exception, and cancellation. Retrieve the result to apply acceptance rules and handle failure separately from CancelledError. Keep a mapping from each future to its operation ID. With as_completed, results arrive by availability rather than input position; zipping them with the original list can assign a response to the wrong client. In a batch report, show accepted results, failures, and tasks that never ran, retaining the references needed for resumption.

Expiring a wait does not terminate a task

result(timeout=2) bounds how long the caller waits. If the function is already running, it may continue after TimeoutError and cancel may return False. A local timeout establishes neither recipient rejection nor rollback. Plan I/O operation deadlines and a cooperative policy for the task to recognize stopping. Exiting with ThreadPoolExecutor waits for shutdown, so catching timeout inside the block does not guarantee immediate return. shutdown with cancel_futures cancels work that has not started; active tasks still require completion and handling of effects.

One budget for the entire path

Define the overall deadline using a monotonic clock and calculate remaining time before waiting, retrying, or backing off. Three five-second attempts with two two-second backoffs can consume nineteen seconds even when the requirement says fourteen. Internal operations must honor the remaining budget; measuring only in the coordinator does not interrupt blocked I/O. The as_completed timeout is measured from the original call and does not automatically renew on each next. Use appropriate timestamps for cross-system audit, but do not interpret an absolute monotonic clock value as a UTC date.

Execution capacity and admission capacity

max_workers limits active tasks, not the future list the producer chooses to create. Submitting millions of items at once can exhaust memory before workers consume the queue. Bound admitted work, release processed results, and avoid first converting all input to a list. In Python 3.14, Executor.map buffersize limits submitted tasks whose results have not yet been yielded; it does not exist in 3.13. chunksize does not replace that control on a thread executor. Also measure each result’s size: a bounded number of objects may still exceed the memory budget.

Dependencies that permit progress

Draw the dependency graph before increasing threads. In a one-worker pool, a task waiting for a subtask submitted to that same pool occupies precisely the capacity the subtask needs to start. Move coordination outside the worker or directly execute a phase that should be sequential. Use bounded experiments instead of creating a permanent suite deadlock. To choose threads or processes, characterize the workload: on conventional CPython with the GIL, CPU-bound Python code does not automatically gain parallelism from more threads. I/O and free-threaded builds have different conditions requiring measurement and explicit compatibility.

from concurrent.futures import ThreadPoolExecutor, as_completed

def inspect_report(report_id):
 if report_id == "R-19":
 raise ValueError("missing required field")
 return {"accepted": True}

outcomes = {}
with ThreadPoolExecutor(max_workers=2) as pool:
 pending = {pool.submit(inspect_report, key): key
 for key in ("R-18", "R-19", "R-20")}
 for future in as_completed(pending):
 key = pending[future]
 try:
 response = future.result
 except ValueError:
 outcomes[key] = "invalid"
 else:
 outcomes[key] = "accepted" if response.get("accepted") is True else "rejected"
assert outcomes == {"R-18": "accepted", "R-19": "invalid", "R-20": "accepted"}
print("3 correlated outcomes; 1 validation failure")
IN PRACTICE

The example uses only three local tasks, so the admitted set is explicitly small. Completion order may change while each result remains associated with its correct ID. Do not generalize it to millions of IDs without bounding submission and retention. In a separate experiment, add a cooperative task and establish that a wait timeout is not cancellation.

Common pitfalls

Counting done as success; positionally matching as_completed results; confusing timeout with rollback; submitting everything before bounding; using buffersize on 3.13; waiting for subtasks on the only worker; promising parallelism without characterizing the workload.

Related topics: Small functions, clear contracts · Errors and files with context · Observable and repeatable automation · Subprocesses, paths, and verifiable results

Take this idea with you

Useful concurrency preserves identity and outcomes, admits only work it can sustain, and provides an explicit policy for deadlines, failures, and shutdown.

Create account

Reference: Python 3.14: Launching parallel tasks · Python 3.14; DR Python 2026.3