I used to reach for whichever Python concurrency tool I'd used most recently on a previous project, regardless of whether it actually fit the specific problem in front of me. Understanding what each of these tools is genuinely built for, rather than treating them as interchangeable, fixed a recurring source of confusing performance bugs where "concurrent" code ran no faster than sequential code.
Python's Global Interpreter Lock means only one thread executes Python bytecode at a time, regardless of how many threads you spin up. This single fact determines which tool actually helps for a given workload, and misunderstanding it is the root cause of most concurrency confusion I've seen in Python codebases.
Threads release the GIL while waiting on I/O, a network request, a file read, a database query, so multiple threads genuinely overlap that waiting time. For CPU-bound work, calculations, image processing, data transformation, threads provide essentially no speedup, since the GIL prevents true parallel execution of Python code regardless of thread count.
import threading
import requests
def fetch(url, results, index):
results[index] = requests.get(url).json()
urls = ["https://api.example.com/1", "https://api.example.com/2"]
results = [None] * len(urls)
threads = [threading.Thread(target=fetch, args=(u, results, i)) for i, u in enumerate(urls)]
for t in threads:
t.start()
for t in threads:
t.join()
Spawning separate processes, each with its own Python interpreter and GIL, genuinely achieves parallel execution across CPU cores. The cost is real overhead, both in process startup time and in serializing data to pass between processes, meaning this only pays off for computation heavy enough to outweigh that overhead.
from multiprocessing import Pool
def cpu_heavy_task(n):
return sum(i * i for i in range(n))
if __name__ == "__main__":
with Pool(processes=4) as pool:
results = pool.map(cpu_heavy_task, [10_000_000] * 8)
Where threading handles a handful of concurrent I/O operations reasonably well, asyncio scales to thousands of concurrent connections far more efficiently, since it avoids the memory and context-switching overhead of actual OS threads entirely, running everything on a single thread through cooperative multitasking instead.
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as response:
return await response.json()
async def main(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
return await asyncio.gather(*tasks)
Calling a synchronous, blocking function, a non-async database driver, a CPU-heavy calculation, directly inside an async function blocks the entire event loop, silently killing all the concurrency asyncio was supposed to provide. I now route genuinely blocking calls through run_in_executor rather than assuming async syntax alone guarantees non-blocking behavior.
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(None, blocking_function, arg)
For a web scraper hitting hundreds of URLs, asyncio consistently wins on both speed and resource usage. For a data pipeline doing genuinely heavy numerical computation across chunks of a large dataset, multiprocessing is the only option that actually delivers parallel speedup. For a simple script making a handful of concurrent API calls where asyncio's learning curve feels like overkill, threading remains a reasonable, simpler choice.
A recent pipeline needed both massive I/O concurrency, fetching data from many sources, and genuine CPU-bound processing on the results. I used asyncio for the fetching stage and handed the processing stage off to a multiprocessing pool, rather than trying to force one tool to handle both halves of a genuinely mixed workload.
Before committing to any of these tools for a specific bottleneck, I now profile the actual code first, confirming whether the slowness is genuinely I/O-bound or CPU-bound, since guessing wrong and reaching for the wrong concurrency model wastes real implementation time solving a problem that turns out not to be the actual bottleneck at all.