Skip to content

concurrent.futures

Playground note: concurrent.futures spawns OS threads/processes — browser sandboxes cannot do this. Run the code snippets locally with python3. The <TsGo>, <Quiz>, and <ProgressTracker> components still work normally.

concurrent.futures is Python’s answer to “I want threads or processes but I don’t want to manage them manually.” It provides a clean, unified API that feels much closer to JavaScript’s Promise-based model than raw threading or multiprocessing.

The two executors are drop-in replacements for each other — you switch between thread-based and process-based concurrency by changing one line.

TypeScript
// TypeScript — Promise.all over an array (built-in)
const tasks = [1, 2, 3, 4, 5].map((n) =>
fetch(`/api/item/${n}`).then((r) => r.json())
);
const results = await Promise.all(tasks);
// No need for explicit thread pools — JS handles it
Python
# Python — concurrent.futures is the closest equivalent
from concurrent.futures import ThreadPoolExecutor
def fetch_item(n: int) -> dict:
import time; time.sleep(0.1) # simulate I/O
return {"id": n, "value": n * 2}
with ThreadPoolExecutor(max_workers=5) as executor:
# executor.map — like Promise.all over an array
results = list(executor.map(fetch_item, [1, 2, 3, 4, 5]))
print(results)
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
def download(url: str) -> str:
time.sleep(0.1) # simulate network I/O
return f"Downloaded: {url}"
urls = [
"https://api.example.com/users",
"https://api.example.com/posts",
"https://api.example.com/tags",
]
# executor.map — results in INPUT order (like Promise.all)
with ThreadPoolExecutor(max_workers=3) as executor:
for result in executor.map(download, urls):
print(result)
print("---")
# executor.submit + as_completed — results in COMPLETION order
with ThreadPoolExecutor(max_workers=3) as executor:
futures = {executor.submit(download, url): url for url in urls}
for future in as_completed(futures):
url = futures[future]
print(f"{url}{future.result()}")

Run this locally — browser sandboxes can’t spawn OS threads/processes.

from concurrent.futures import ProcessPoolExecutor
def is_prime(n: int) -> bool:
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
if __name__ == "__main__":
numbers = list(range(1_000_000, 1_000_100))
# ThreadPoolExecutor: limited by GIL — no speedup for CPU work
# ProcessPoolExecutor: each process has its own GIL → real parallelism
with ProcessPoolExecutor(max_workers=4) as executor:
primes = [n for n, p in zip(numbers, executor.map(is_prime, numbers)) if p]
print(primes[:5])

Future objects — inspect individual results

Section titled “Future objects — inspect individual results”
from concurrent.futures import ThreadPoolExecutor, Future
import time
def task(n: int) -> str:
time.sleep(n * 0.01)
return f"result-{n}"
with ThreadPoolExecutor(max_workers=3) as executor:
f1: Future[str] = executor.submit(task, 1)
f2: Future[str] = executor.submit(task, 2)
f3: Future[str] = executor.submit(task, 3)
# .result() blocks until done — like await promise
print(f1.result()) # "result-1"
print(f2.result()) # "result-2"
# Check state without blocking
print(f3.done()) # True/False
print(f3.running()) # True/False

Choosing between ThreadPoolExecutor and ProcessPoolExecutor

Section titled “Choosing between ThreadPoolExecutor and ProcessPoolExecutor”
TypeScript
// TypeScript
// No explicit choice needed — the event loop handles I/O,
// Web Workers handle CPU. The runtime decides threading.
Python
# Python — you choose explicitly
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
# I/O-bound (network, disk, DB calls with blocking libraries)
with ThreadPoolExecutor(max_workers=20) as executor:
results = list(executor.map(blocking_http_call, urls))
# CPU-bound (math, compression, parsing, ML inference)
with ProcessPoolExecutor(max_workers=4) as executor:
results = list(executor.map(cpu_intensive_fn, data))
# Rule of thumb for max_workers:
# Threads: 5 * os.cpu_count() (I/O-bound — threads mostly sleep)
# Processes: os.cpu_count() (CPU-bound — one per core is enough)
What is the main advantage of `concurrent.futures` over raw `threading` or `multiprocessing`?
What is the difference between `executor.map()` and `as_completed()`?
Which executor should you use for CPU-bound work to achieve true parallelism?
How do you run a blocking synchronous function inside an asyncio event loop without blocking the loop?