Deciding how to parallelise Python, and preparing code for a GIL-free future, depends on these details.
The global interpreter lock is a mutex in CPython that a thread must hold to execute bytecode. It exists mainly because reference counting and much of the interpreter's internal state aren't thread-safe; one big lock made the single-threaded case fast and C extensions simple to write.
How it behaves:
- a running thread is asked to drop the GIL after a switch interval (
sys.getswitchinterval(), 5 ms by default) so others can run - blocking I/O and
time.sleeprelease it, which is why threads help with I/O-bound code - C extensions can release it around long computations — NumPy, hashlib and compression do — so some "CPU" work does parallelise across threads
What it does not do is make your code thread-safe. A switch can happen between any two bytecodes, so x += 1, check-then-act sequences and multi-step updates race. Individual operations on built-ins such as list.append happen to be atomic, but don't design around that.
Python 3.13 ships an experimental free-threaded build (PEP 703), available as an optional python3.13t executable in the official Windows and macOS installers, or built from source with --disable-gil. It replaces the GIL with biased reference counting, per-object locks and deferred reclamation, so threads run Python code on multiple cores. In 3.13 it is explicitly experimental: single-threaded code runs noticeably slower (the specialising interpreter is disabled in that build), and importing an extension module that hasn't declared free-threading support re-enables the GIL with a warning. You can force the choice with PYTHON_GIL=0 or -X gil=0, and check at runtime with sys._is_gil_enabled().
Code that is correctly synchronised with locks and queues runs unchanged on either build; code that leaned on the GIL for accidental safety is what breaks.
Example
import sys
import sysconfig
import threading
import time
free_threaded_build = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
gil_on = getattr(sys, "_is_gil_enabled", lambda: True)()
print(f"free-threaded build: {free_threaded_build}, GIL enabled: {gil_on}")
print("switch interval:", sys.getswitchinterval()) # 0.005
def spin(n=3_000_000):
total = 0
for i in range(n):
total += i
return total
for workers in (1, 4):
threads = [threading.Thread(target=spin) for _ in range(workers)]
start = time.perf_counter()
for t in threads: t.start()
for t in threads: t.join()
print(workers, "threads:", round(time.perf_counter() - start, 2), "s")
# With the GIL, 4 threads take ~4x as long as 1 (no parallelism).
# On python3.13t with the GIL off, they take about as long as 1.Remember
- The GIL lets one thread run bytecode at a time; I/O and some C code release it
- The GIL never made compound operations like
x += 1atomic - 3.13's free-threaded build (python3.13t) is experimental and opt-in
Watch out for
- Assuming code is thread-safe because of the GIL.