The idea: one lock around the interpreter

CPython, the standard Python, has a Global Interpreter Lock (GIL). A thread must hold the GIL to run Python bytecode. There is one GIL per interpreter, so only one thread runs Python code at any moment, even on a machine with many cores. The threads are real OS threads; they just take turns on the lock.

Three timelines for three CPU-bound threads of 8 ms each. Threads with the GIL: T1, T2 and T3 run one at a time in 5 ms turns and finish at 24 ms. multiprocessing: three processes each spend 2 ms starting, then run in parallel, done at 10 ms. Free-threaded 3.13t: three threads run in parallel from the start, done at 8 ms.
With the GIL, three CPU-bound threads take turns and need 24 ms; with a GIL per process or no GIL at all they run in parallel.

So threads in CPython are concurrent but, for Python code, not parallel. See Concurrency vs Parallelism for the difference.

Why CPython has a GIL

Every Python object has a reference count that changes all the time (a = b adds one, leaving a function drops one). If two threads changed the same count at once, an update could be lost and an object freed while still in use. One big lock makes every count, dict and list safe without a lock per object, keeps single-threaded code fast, and lets C extensions assume nobody else is touching Python objects. The GIL is a mutex with a special hand-off rule.

Taking turns: the switch interval

A thread that wants the GIL waits on a condition variable with a timeout of one switch interval: sys.getswitchinterval(), 0.005 s (5 ms) by default. If the GIL is still held by the same thread when the timeout ends, the waiter sets gil_drop_request. The holder checks that flag in its eval loop between bytecodes; it drops the GIL, and the waiter takes it. The holder then joins the waiters.

Time runs down. T1 holds the GIL and runs bytecode. T2 asks for the GIL at 0 ms and waits. At 5 ms the wait times out, T2 sets gil_drop_request. T1 sees the flag at its next bytecode check, drops the GIL and waits. T2 takes the GIL and runs. Separately, a thread calling recv() releases the GIL before blocking and asks for it again when the data arrives.
A waiting thread forces a hand-off after one switch interval; a blocking call gives the GIL away at once.

Three CPU-bound threads of 8 ms each take turns of 5 ms and finish at 24 ms, exactly as long as running them one after another (Demo: CPU-bound threads take turns). With a 2 ms interval (sys.setswitchinterval(0.002)) there are more hand-offs, but the total is still 24 ms (Demo: switch interval 2 ms). A real switch also costs a little time and cache warmth, so many threads can be slower than one.

CPU-bound vs I/O-bound threads

Blocking calls such as socket.recv, file.read and time.sleep release the GIL before they block (Py_BEGIN_ALLOW_THREADS in C) and take it back when they return. While one thread waits for the network, another can run Python code. Three threads that each do 1 ms of work, 8 ms of I/O and 1 ms of work finish in 12 ms instead of 30 (Demo: I/O-bound threads overlap their waits).

workload (3 threads)one after anotherthreads + GILmultiprocessingfree-threaded
CPU-bound, 3 × cpu 824 ms24 ms10 ms8 ms
I/O-bound, 3 × (cpu 1, io 8, cpu 1)30 ms12 ms12 ms10 ms

The convoy effect

Mixing the two kinds hurts the I/O thread. Each time its data arrives it needs the GIL again, but a CPU-bound thread holds it and will not let go until the switch interval runs out. An I/O thread that needs 10 ms alone finishes much later next to a CPU hog (Demo: a CPU hog delays an I/O thread). On a server this shows up as extra latency for every request handled by a thread that shares the process with heavy computation.

Ways around the GIL

  • multiprocessing / ProcessPoolExecutor: each worker is a separate process with its own interpreter and its own GIL, so CPU-bound work runs in parallel. The price is starting processes and pickling arguments and results across a pipe (Demo: multiprocessing: 2 ms to start, then 8 ms in parallel).
  • C extensions that release the GIL: NumPy, hashlib and zlib drop the GIL inside long C loops, so threads calling them run in parallel.
  • Free-threaded Python (PEP 703): CPython 3.13 ships an optional build, python3.13t, with no GIL. Objects are protected by per-object locks, and reference counts use biased and atomic counting. CPU-bound threads run in parallel (Demo: free-threaded 3.13t). Single-threaded code is somewhat slower in this build, and C extensions must be marked as safe without the GIL. Check with sys._is_gil_enabled().
  • Subinterpreters (PEP 684, 3.12): several interpreters in one process, each with its own GIL.

The GIL does not make your code thread-safe. It protects the interpreter's own data, but counter += 1 is several bytecodes, and a switch can happen between them. Use a threading.Lock (see Mutex) around shared updates.

What the page leaves out

Time moves in 1 ms ticks and a switch costs nothing. Real CPython checks the eval breaker only at certain bytecodes, and the OS decides which waiting thread wakes first; the page uses a simple FIFO. It leaves out the cost of switching and of cache misses, the pre-3.2 "old GIL" that switched every 100 ticks, asyncio (one thread, no GIL contention), the details of biased reference counting, and the exact overhead of the free-threaded build.