concurrent.futures provides one API for submitting functions to thread or process pools. Threads generally suit work waiting on network or disk; processes can help with serializable CPU work that is large enough to justify their overhead.
Consume results as they finish
from concurrent.futures import ThreadPoolExecutor, as_completed
from urllib.request import urlopen
def size(url: str) -> tuple[str, int]:
with urlopen(url, timeout=5) as response:
return url, len(response.read(1_000_000))
urls = ["https://www.python.org/", "https://docs.python.org/3/"]
with ThreadPoolExecutor(max_workers=4) as executor:
futures = [executor.submit(size, url) for url in urls]
for future in as_completed(futures):
try:
print(future.result())
except Exception as error:
print(f"failed: {error}")
Set timeouts inside the operation as well. Timing out while waiting for a Future does not automatically stop a network call. Limit input, response size, and worker count so concurrency does not overwhelm memory or external services.
Avoid a pool task waiting for another task in the same saturated pool, which can deadlock. With ProcessPoolExecutor, functions and arguments must be serializable, and entry points require the usual protection on platforms that start processes by importing the module.
For thousands of cooperative connections, Python async and await may fit better. The official concurrent.futures documentation, accessed July 22, 2026, covers executors, futures, cancellation, and deadlock hazards.
Choose from measured work
ThreadPoolExecutor fits functions that mostly wait for sockets, files, databases, or subprocesses. Threads share memory, so arguments are cheap to pass, but mutable state needs synchronization. CPU-bound pure Python usually does not gain parallel execution from threads on conventional CPython. ProcessPoolExecutor provides separate interpreters and memory spaces, with startup and serialization costs.
Measure a complete batch. Processes can lose to sequential execution when tasks are tiny or arguments are large. Some native libraries release the GIL, so threads may help numerical work. Executor choice follows observed workload, not a slogan.
Ordering and error handling
executor.map() yields results in input order. submit() with as_completed() exposes completion order and lets each future retain context:
from concurrent.futures import ThreadPoolExecutor, as_completed
items = [" Alpha ", "Beta", " gamma"]
with ThreadPoolExecutor(max_workers=3) as executor:
pending = {executor.submit(str.strip, item): item for item in items}
for future in as_completed(pending):
original = pending[future]
try:
print(original, future.result())
except Exception as exc:
print(f"{original!r} failed: {exc}")
result() re-raises the worker exception. Always observe futures, otherwise partial failure can remain hidden. Log input identity without exposing secrets.
Bound submission and downstream pressure
A worker limit does not bound memory if a million futures are submitted immediately. Feed work in windows, use a bounded producer queue, or use buffering available in the supported Python version. Match concurrency to downstream capacity. Fifty threads cannot improve a database limited to ten connections.
A waiting timeout does not reliably stop running code. Network and database calls need their own timeouts. Cooperative jobs can inspect a threading.Event at safe boundaries and return cleanly.
Cancellation and shutdown
future.cancel() succeeds only before execution begins. A running thread cannot be safely killed. Keep operations short and idempotent where possible. The executor context manager waits for submitted work. On failure, shutdown(cancel_futures=True) can cancel queued work while running tasks finish.
Process workers must import the main module. Protect pool creation with if __name__ == "__main__":, especially on spawn-based platforms. Submit top-level functions and serializable values. Lambdas, nested functions, locks, open handles, and live clients commonly fail serialization.
from concurrent.futures import ProcessPoolExecutor
def count_divisors(number: int) -> int:
return sum(number % divisor == 0 for divisor in range(1, number + 1))
def main() -> None:
with ProcessPoolExecutor(max_workers=2) as executor:
print(list(executor.map(count_divisors, [50_000, 50_001])))
if __name__ == "__main__":
main()
Avoid repeatedly sending large immutable data. Use worker initialization when appropriate and tasks large enough to amortize serialization.
Prevent deadlocks and races
A task must not wait for another future queued in the same saturated pool. Keep orchestration in the caller or separate dependency stages. Prefer workers that return values over workers that mutate shared structures. The caller can combine results deterministically.
Pool size controls simultaneous workers, not requests per second. A rate-limited service needs an explicit limiter. Retries also consume capacity, so cap attempts and apply backoff outside the worker when possible.
Test concurrent behavior
Start with correct sequential code and inject the executor when practical. Test worker exceptions, timeouts, partial completion, cancellation, and shutdown. Avoid relying on exact completion order unless that is the contract. Integration tests should use controlled local dependencies instead of public services. Profiling, explicit resource budgets, and contextual logs turn concurrency into a measurable optimization rather than hidden scheduling logic.
Model results explicitly
Returning a small immutable result object is often clearer than printing or mutating global state inside a worker. The result can carry the item identifier, elapsed time, value, and a domain status. The coordinating thread then decides what to persist and how to report partial success. This reduces lock usage and prevents interleaved output from becoming the only record of what happened.
Exceptions should remain exceptional. If "not found" is an expected business outcome, return a typed result rather than raising it thousands of times. For unexpected failures, retain the original exception chain and associate it with the submitted item. Never catch Exception inside a worker merely to return None, because the caller cannot distinguish failure from a legitimate empty value.
Chunking and fairness
Process pools benefit from batching small inputs, but oversized chunks create poor load balancing: one worker may receive all expensive items while others become idle. Benchmark representative, uneven data. Threads used for remote calls may also need separate pools when one slow dependency could monopolize every worker needed by another.
Priority is not part of the basic executor contract. If urgent and background jobs compete, use separate bounded executors or a scheduler designed for priorities. Avoid an unbounded global pool shared by unrelated request handlers. It makes latency depend on hidden work elsewhere in the application.
Context, logging, and observability
Thread workers share process logging configuration, while process workers have distinct memory and may need explicit initialization. Pass correlation identifiers as ordinary serializable values. Context variables do not automatically provide a universal cross-process propagation mechanism.
Record queue delay separately from execution duration when latency matters. A fast function can still deliver a slow result after waiting in an overloaded pool. Useful metrics include submitted, running, completed, failed, cancelled, queue wait, and task duration. Avoid high-cardinality labels such as raw URLs or user identifiers.
Database and client ownership
Do not pass an open database connection, HTTP client, or transaction into a process worker. Such objects are generally not serializable and may be unsafe after a fork. Create process-local resources through supported initialization and close them during worker teardown where the library permits it. With threads, first confirm that a client is documented as thread-safe; otherwise create one per worker or protect access.
A transaction should normally remain in the coordinating code. Parallel workers can compute or fetch independent values, then the caller validates the complete set and commits deliberately. This avoids leaving half a batch persisted when one future fails.
When another abstraction fits better
Executors are local, in-process coordination. They do not provide durable queues, retries across restarts, distributed scheduling, or exactly-once processing. Use a job system when tasks must survive process failure or run on multiple machines. Use asyncio when a large number of operations already expose cooperative async APIs. Use vectorized or native code when the real goal is efficient numerical computation.
The simplest acceptable design is often a bounded executor around a synchronous library. Make ownership, shutdown, error policy, and capacity visible at the call site. That keeps concurrency a controlled implementation choice rather than an accidental property of every function.