Under The Hood
backend python CPython 3.12+ (3.13 free-threaded builds exist)

Python: the GIL and asyncio

Last updated
Prerequisites:
Python threading and async basics
Understanding of OS threads and event loops
  • python
  • gil
  • asyncio
  • concurrency
  • cpython
  • threading

Read at your depth

The practical view

The GIL (Global Interpreter Lock) is a mutex in CPython that allows only one thread to execute Python bytecode at a time. That is why threading in CPython does not speed up CPU-bound work: threads interleave but never run in parallel on multiple cores. For I/O-bound work (network, disk), threads still win because the GIL is released during blocking calls. asyncio is single-threaded cooperative concurrency: async def functions, await points, and an event loop (asyncio.run(...)) that switches between coroutines when they await I/O. Use asyncio for high-concurrency I/O; use multiprocessing (separate interpreters, no GIL sharing) for CPU-bound parallelism.

Legacy vs modern

Thread pool for CPU work vs asyncio for I/O work

Threads cannot parallelize Python bytecode under the GIL; asyncio scales concurrent I/O on a single thread without OS-thread overhead.

before → after
Thread pool for CPU-bound work
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(8) as pool:
    results = list(pool.map(cpu_heavy, data))
asyncio for concurrent I/O
async def main():
    async with aiohttp.ClientSession() as session:
        results = await asyncio.gather(*(fetch(session, u) for u in urls))

Interview gotchas

Context

The interviewer wants the mechanism, not the slogan — reference counting, bytecode serialization, and the switch interval are the key details.

The mechanical answer

CPython serializes bytecode execution with the GIL: at any moment one thread owns the interpreter (acquired for ~5ms of bytecode or until a blocking call). CPU-bound Python therefore interleaves rather than parallelizes. The GIL protects the interpreter's invariants: object reference counts (a racing Py_DECREF could free an object still in use), the cyclic GC's state, and global interpreter structures. It is released around blocking C-level operations (socket reads, file I/O), which is why I/O-bound threading still scales. PEP 703 free-threaded builds remove the GIL by making refcounting per-object (immortal objects) and adding fine-grained locks — at a performance cost for single-threaded workloads.

Trap

The naive answer is 'the GIL prevents race conditions in user code' — it only protects interpreter internals, not your code (your data races still exist). Another trap: 'threads are useless in Python' — they are correct and valuable for I/O-bound concurrency; the GIL is released during blocking calls. Mentioning the switch interval and Py_BEGIN_ALLOW_THREADS demonstrates you know how the lock is actually released and reacquired.

Under The Hood — a multi-depth technical interview hub.

Third-party licenses Press ⌘ K to search.