Skillversitas

ThreadPoolExecutor

Creating and joining a handful of threads by hand gets noisy fast. The concurrent.futures.ThreadPoolExecutor manages a pool of worker threads for you: you submit callables, the pool runs them on idle workers, and you collect results through Future objects. It is the standard way to run many I/O tasks concurrently in a few lines.

map for Homogeneous Work

from concurrent.futures import ThreadPoolExecutor
import time

def fetch_page(url):
    # simulate a network call
    time.sleep(0.5)
    return f"content of {url}"

urls = [f"https://example.com/page/{i}" for i in range(10)]

with ThreadPoolExecutor(max_workers=5) as pool:
    results = list(pool.map(fetch_page, urls))

print(len(results))  # 10 results, ~2s total instead of ~5s

map preserves input order in the output and streams results as they become available. The pool size should match the resource you are hammering - 5 workers for an API limited to 5 concurrent connections, more for local disk I/O.

submit for Heterogeneous Work

When each task is different, use submit, which returns a Future immediately:

from concurrent.futures import ThreadPoolExecutor, as_completed

def load(key, delay):
    time.sleep(delay)
    return key, delay * 10

with ThreadPoolExecutor(max_workers=4) as pool:
    futures = {pool.submit(load, k, d): k for k, d in
               [("fast", 0.1), ("slow", 0.5), ("medium", 0.3)]}

    for fut in as_completed(futures):
        key, value = fut.result()
        print(key, value)   # prints in completion order, not input order

as_completed yields futures as they finish, which is what you want when you should process results eagerly (update a UI, flush to a database) rather than wait for the slowest task. Call fut.result() to retrieve the value or re-raise the exception the task raised.

shutdown

The context manager calls shutdown(wait=True) automatically. Without it: shutdown(wait=False) lets already-submitted work run while refusing new submissions, and cancel_futures=True (Python 3.9+) also cancels queued-but-not-started tasks. Threads in the pool are joined on shutdown, so a with block is the idiomatic scope for a pool.

Summary

Use ThreadPoolExecutor instead of manual thread bookkeeping: map for a batch of identical calls, submit plus as_completed when you need per-task futures and eager processing, and size the pool to the bottleneck, not to the number of cores.