Doing more than one thing "at once" in Python is shaped by one specific detail most other languages don't have: the GIL.
The GIL allows only one thread to execute Python bytecode at a time, even on a multi-core machine — a genuine limitation for CPU-heavy work split across threads, but far less of one for work that spends most of its time waiting (a network request, a file read), since the GIL is released during that wait.
import threading
import time
def download(name):
print(f'Starting {name}')
time.sleep(2) # stands in for a slow network request
print(f'Finished {name}')
threads = [threading.Thread(target=download, args=(f'file{i}',)) for i in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()
# All 3 "downloads" run concurrently — total time ~2s, not ~6sWhat threading does not fix
Because of the GIL, adding more threads does not speed up CPU-heavy computation (heavy math, image processing) — only genuinely I/O-bound waiting benefits. multiprocessing (separate processes, each with its own GIL) is the right tool for CPU-heavy parallelism, a topic beyond this lesson.
asyncio handles many waiting operations on a single thread, switching between them whenever one is waiting — no threads at all, similar in spirit to the event loop model from the JavaScript course.
import asyncio
async def download(name):
print(f'Starting {name}')
await asyncio.sleep(2)
print(f'Finished {name}')
async def main():
await asyncio.gather(
download('file1'),
download('file2'),
download('file3'),
)
asyncio.run(main())
# Also ~2s total, no threads involvedasync def marks a function as a coroutine — it can be paused at an await and resumed later, without blocking anything else. asyncio.gather() runs several coroutines concurrently and waits for all of them.
| threading | asyncio |
|---|---|
| Works with libraries that were never written with async in mind | Needs libraries built for it specifically (an async database driver, aiohttp instead of requests) |
| Real OS threads, some overhead per thread | Lighter weight — thousands of coroutines are practical, threads are not |
| Simpler to reason about for a handful of concurrent tasks | The standard modern choice for a server handling many concurrent connections |