A Python Task Can Finish Second in the Input List and First in the Result Stream
Use Python futures without confusing submission order with completion order. Preserve result identity, inspect failures and understand timeout and cancellation limits.
A program starts two independent jobs. The second finishes quickly, but the display waits for the first result before showing anything. The work may be concurrent even though the chosen result interface presents it in input order.
That distinction matters when downloading independent files, checking records or building a progress display. Submission order, completion order and final presentation order are three separate choices. A reliable program retains enough identity to translate between them.
The established Python concurrent.futures documentation, checked on 27 October 2026, provides both ordered mapping and completion-based iteration. This tutorial uses Python 3.12-compatible standard-library interfaces. It is not a benchmark of the latest Python release or a claim that more threads always make a workload faster.
Choose the order the consumer actually needs
An ordered report often needs rows aligned with the original input. A progress display may benefit from observing whichever task finishes next. Those requirements can coexist if the program stores identity instead of assuming arrival position identifies the source.
Executor.map returns results in input order. as_completed yields completed futures, with no promise of input order. For futures already complete when iteration begins, do not assume an exact historical chronology among them.
| Consumer requirement | Candidate interface | Information to preserve |
|---|---|---|
| Ordered output | Executor mapping or explicit reconstruction | Original input position |
| Progress as jobs finish | as_completed |
Future-to-input mapping |
| Auditable partial batch | Explicit outcome records | Task identity and status |
The interface choice should follow the consumer's contract rather than an unqualified preference for “parallel” behavior.
Keep result identity outside arrival order
Suppose the inputs are records A and B. If B finishes first, appending its value to a list and later pairing that list with the original inputs can assign B's result to A.
Map each future to a stable input index or business identifier at submission. Use that mapping when processing either success or failure. For this small demonstration, input indices are sufficient and avoid collisions if display labels happen to repeat.
A business identifier still needs its own uniqueness and authorization rules in a real application. A Python dictionary does not establish that two source records truly describe different invoices or customers.
Run a deterministic two-job example
The example holds A on a gate while B can finish. The main thread observes B, then releases A in a finally block. This avoids using arbitrary sleep durations as evidence of scheduling order.
The threading Event documentation describes the synchronization primitive. The timeout is a guard against an unexpected stalled demonstration, not a performance target.
from concurrent.futures import ThreadPoolExecutor, as_completed
from threading import Event
release_a = Event()
def job_a():
if not release_a.wait(timeout=5):
raise TimeoutError("The demonstration gate was not released")
return "result-A"
with ThreadPoolExecutor(max_workers=2) as executor:
future_a = executor.submit(job_a)
future_b = executor.submit(lambda: "result-B")
identities = {future_a: 0, future_b: 1}
results = [None, None]
observed = []
stream = as_completed(identities, timeout=5)
try:
first = next(stream)
assert first is future_b
position = identities[first]
results[position] = first.result()
observed.append(position)
finally:
release_a.set()
for future in stream:
position = identities[future]
results[position] = future.result()
observed.append(position)
assert observed == [1, 0]
assert results == ["result-A", "result-B"]
print("Observed positions:", observed, "Ordered results:", results)
The first observation is B because A cannot finish before the release. The final list nevertheless follows input order because each result is placed using its recorded position.
Expect errors when retrieving the result
A completed future can contain an exception. Completion is not the same as successful work. Calling future.result() is where a worker exception is re-raised to this consumer.
For a real partial-batch workflow, define explicit states such as succeeded, failed and cancelled. A placeholder of None is not sufficient if None can also be a legitimate successful value. The demonstration uses placeholders only for a two-success case with known string results.
Decide whether one failure should stop the whole batch or whether the program should collect every outcome for reconciliation. That is an application policy, not a decision that the iterator can make for you.
Separate a waiting timeout from stopping work
A timeout on a result wait limits how long the caller waits under that interface. It does not automatically kill the underlying operation. as_completed measures its timeout from the call to that iterator; it is not a fresh independent allowance for each future.
Likewise, cancellation can succeed for work that has not started, while a running task generally cannot be cancelled through Future.cancel(). An external network call may need its own timeout and a cooperative shutdown design.
A report should distinguish “not received before this deadline” from “confirmed not executed.” Those states can require different recovery actions when the task has side effects.
Give executor shutdown an explicit role
The context manager waits for executor work during shutdown. A caller can therefore encounter a timeout and still wait while leaving the context if work remains active. Do not promise a hard end-to-end deadline solely because one wait call has a timeout argument.
The controlled example releases its gate in finally so a failed assertion does not unnecessarily leave A waiting for the guard timeout. A production design needs equally explicit cleanup for the resources its jobs use.
Keep slow or unavailable dependencies in the test plan. A program that handles successful completion correctly may still behave poorly when one task never reaches its expected stopping condition.
Bound the submitted work as well as the workers
A worker limit does not necessarily bound the number of queued tasks or stored futures. Building a future for every row of a very large input can consume substantial memory even when only a few run at once.
For larger pipelines, choose a bounded submission strategy appropriate to the supported Python version and retain only the state needed for the contract. This article's eager two-job dictionary is intentionally small; it is not advertised as a streaming implementation.
Also examine what the jobs do. Thread-based concurrency can help suitable I/O workloads. It does not guarantee a speedup for CPU-heavy Python code in a conventional GIL-enabled runtime. Measure the actual bottleneck before changing concurrency.
Test the identity and recovery contract
Use controlled jobs that complete in a different order, raise an exception, remain pending until cancelled and wait beyond a caller deadline. Verify the final task-to-result mapping and the explicit state of every input.
Then exercise the real workload's timeouts and side effects in an authorized test environment. Correctly ordering two strings does not prove an external operation is safe to retry.
For related boundaries, see strict zip and partial results and idempotent payment effects. Each separates evidence received by the program from work actually performed elsewhere.
Sources and limits
- Python 3.12 concurrent.futures reference, checked 27 October 2026.
- Python threading Event reference, checked 27 October 2026.
The example verifies ordering and identity under a controlled gate. It makes no throughput, resource-usage or distributed exactly-once guarantee.