Python GIL: What It Blocks and What 3.14t Changes

The Python GIL is a CPython mutex: one OS thread runs bytecode at a time. Default 3.14 still has it; 3.14t is supported opt-in.

Updated 10 min read
Code editor showing HTML and CSS on a dark screen

The Python GIL (Global Interpreter Lock) is a CPython mutex that lets one OS thread run Python bytecode at a time. Default 3.14 still has it. The free-threaded 3.14t build is supported opt-in under PEP 779 as of June 2025, not the default.

The Python Software Foundation ships CPython. The Steering Council accepted PEP 703 (Sam Gross) in October 2023 and PEP 779 in June 2025. If you spawn threads for CPU-bound Python, they serialize; blocking I/O and well-behaved C already drop the lock.

Key Takeaways

  • The GIL is a process-wide mutex in CPython. One OS thread executes Python bytecode at a time.
  • It blocks parallel Python bytecode. It does not block overlapping I/O, well-behaved C extensions, or separate processes.
  • Default 3.14 still has the GIL. Free-threaded 3.14t is officially supported and still optional.
  • On independent CPU-bound Python, Miguel Grinberg measured about 3.1× on four threads. Single-thread tax on 3.14 is roughly 5–10%.
  • Reach for free-threading when the work is CPU-bound Python on independent data and your C extensions have opted in. Stay on the default build for I/O-bound scripts and request handlers that do not parallelize.

What Is the Python GIL?

The global interpreter lock is a mutex that protects access to Python objects, so multiple threads cannot execute Python bytecode at once. People say “Python has a GIL.” They mean CPython.

Jython and IronPython have no GIL. PyPy still has one.

Threads still exist under the GIL. They take turns on bytecode via sys.setswitchinterval(). A thread must hold the GIL before it touches Python objects or calls the C API.

Guido van Rossum added the lock in 1992, when typical machines had one core. The design traded a lock on every object for one process-wide mutex, which kept the C API simple and reference counting cheap.

What the GIL Actually Blocks

The precise cut is whether this thread is executing CPython bytecode right now.

Holds the GIL (serialized)

Releases the GIL (can overlap)

Pure-Python loops, attribute access, int math

Blocking I/O (socket.recv, HTTP, sleep, files)

Two Python threads counting primes

Well-behaved C (Py_BEGIN_ALLOW_THREADS)

CPU-bound Python on 16 cores → ~1 core of bytecode

NumPy inner loops, hashlib-in-C, image codecs

A C extension that never drops the lock

multiprocessing (separate interpreters, separate GILs)

On r/Python, the recurring correction is still the CPU/I/O split.

"GIL only allows one CPU-bound thread to execute at a time. I/O bound threads can multi-thread." (u/gerardwx in r/Python, Nov 2025)

A C extension that holds the lock inside one long call looks GIL-bound and is not a Python-bytecode problem. C-API threads document the other side: the lock is released around blocking I/O via Py_BEGIN_ALLOW_THREADS.

The GIL does not make your code thread-safe. Thomas Wouters, speaking at PyCon US 2026, said you have no control over when Python releases and then reacquires the GIL, and that it mostly does not protect C or C++ extensions either.

PEP 684 subinterpreters (3.12+) give you one GIL per interpreter, not one GIL gone. That is a different multi-core path, not free-threading.

How the GIL Works

CPython’s memory model is reference counting plus a shared object graph. Without a lock, two threads could increment, decrement, or deallocate the same object. One mutex around bytecode was cheaper than a lock on every PyObject.

PEP 703 states the cost in 2023 language: the GIL prevents multiple threads from executing Python code at the same time, and that is an obstacle to using multi-core CPUs from Python efficiently. The motivating case was scientific and AI inter-operator parallelism: lots of small Python steps between native kernels.

Sam Gross, who wrote PEP 703, said in a EuroPython keynote that only one thread can run Python code at a time. NumPy, PyTorch, and scikit-learn already hide parallelism inside a call by releasing the lock around work that does not touch Python objects.

The remaining pain is Python itself: worker threads that still have to run bytecode, or pipelines made of many small Python steps. If this lock is your first look at CPython internals, start with Python basics before you chase cores.

The Free-Threaded Build

Free-threaded CPython is a separate binary. Installing Python still defaults to the normal build. Prefer the name free-threaded over “nogil”.

Thomas Wouters told PyCon US 2026 the Steering Council rejected “no-GIL” so they would not have to talk about a “no no-GIL build.”

3.13 vs 3.14

Phase

Meaning

When

I

Experimental build-time option. Single-thread tax 20–40%

3.13

II

Officially supported, still optional. Single-thread tax about 5–10%

3.14 (PEP 779, 16 Jun 2025)

III

Free-threaded default

Has not happened

3.13 shipped --disable-gil as an experiment. 3.14 is Phase II: supported, still optional. Phase III (default) needs a future PEP.

Charlie Marsh reported the label change when 3.14 stabilized.

As of Python 3.14, the free-threaded (or no-GIL) version of the Python interpreter is no longer considered experimental. https://t.co/AeY7IZFTPU
Charlie Marsh · @charliermarshView on X

Install, identify, runtime flags

Official macOS and Windows installers expose a checkbox. From Astral, the uv path is a t suffix on the version:

Shell
uvx python@3.14t

That is the uv toolchain path for pins and tools. Identify the build with python -VV (the version string contains “free-threading build”), sysconfig.get_config_var("Py_GIL_DISABLED") == 1, and the runtime check:

Python
import sys

print(sys._is_gil_enabled())

False means the process is actually running without the lock. True on a 3.14t binary means something turned it back on.

Free-threaded builds can re-enable the GIL at runtime with PYTHON_GIL or -X gil. -X gil=0 / PYTHON_GIL=0 force it off. Unmarked C-API modules re-enable the GIL for the whole process and print a warning.

A PyO3 module declared with gil_used = true re-enables the GIL, and the interpreter prints a RuntimeWarning. Always re-check sys._is_gil_enabled() after imports. Wouters called forcing the GIL off with an unmarked extension playing with fire, something you probably only want for testing.

Dict, list, and set take internal locks that behave like the GIL for those types. That is not a language guarantee. Use threading.Lock for anything you share.

Do not read another thread’s frame.f_locals. Iterators are not concurrent-safe. ContextVars thread_inherit_context defaults true on free-threaded builds and false on the GIL build.

Subinterpreters remain the other path while the default build keeps the GIL. 3.14’s InterpreterPoolExecutor is shared-nothing, one GIL per interpreter, and uses fewer system resources than processes. On r/programming, u/vk6_ (Oct 2025) reported about 30% RAM savings moving a web server from multiprocessing to one-thread-per-core subinterpreters, without taking the free-threaded build.

Real Workloads Under Free-Threading

Free-threading is worth it when the work is CPU-bound Python on independent data and C extensions have opted in. Single-thread scripts, Django-style request handlers that do not parallelize, and anything that mutates shared Python objects pay a tax. I/O-bound asyncio on the default GIL build already releases the lock, so free-threading buys nothing there.

Optiver draws the same split: free-threading for CPU-parallel work, async for I/O concurrency. asyncio.to_thread() is still GIL-bound for CPU on the default build. Asyncio on free-threaded Python wants one event loop per thread, not a shared loop.

When threads win

Miguel Grinberg (8 Oct 2025) ran pure-Python benchmarks on 3.14 and 3.14t, with four threads each doing independent work:

Benchmark (seconds, Linux / macOS)

GIL 1T

GIL 4T

FT 1T

FT 4T

Fibonacci, fibo(40)

6.59 / 6.39

32.60 / 24.96

7.05 / 7.27

10.80 / 7.81

Bubble sort, 10,000 numbers

2.18 / 2.05

10.55 / 8.27

2.66 / 2.28

6.23 / 3.02

Four Fibonacci threads ran about 3.1× faster on the free-threaded build, and bubble sort about 2×. On the GIL build, four threads took four to five times as long as one: zero multi-thread gain.

arXiv 2603.04782 (CPython 3.14.2) saw up to 4× time and energy on parallel independent data. Sequential runs used 13–43% more energy with no speedup. Shared-object mutation reduced the gains or reversed them.

Energy tracked wall time. Memory went up with per-object locks and a new allocator.

Sam Gross’s Hanabi trainer is the research-workload analog. With the GIL on, extra workers added no games per second. After removing it and sharding a pybind11 table, the system hit nearly 1,200 games/sec at 60 workers, about 20× versus the GIL build.

On r/Python, u/phovos (Nov 2025) posted a 14-queen bench via uv run --python python3.14t: GIL-enabled about 7.07s–7.51s from 1–8 threads; free-threaded 7.62s down to 1.82s at 8 threads. Textbook CPU-bound pure Python. Not how NumPy-class libraries work.

Quansight measured an asyncio TCP benchmark at 532 MB/s on the default build versus 1,455 MB/s free-threaded with 6 workers, and 698 versus 1,924 MB/s with 12, using one event loop per thread.

The single-thread tax

The free-threading howto measures pyperformance at about 1% on macOS aarch64 to 8% on x86-64 Linux. What’s New 3.14 says roughly 5–10%. PEP 779 cites about 10% on Linux/Windows, about 3% on macOS, with a hard ceiling of 15%.

Grinberg’s 3.14 free-threaded fibo ran at 91% of standard (7.05s vs 6.59s on Linux). That is fibo, not pyperformance, so keep it separate from the howto’s 1–8%.

3.13’s tax was a different world: the Language Summit put it at 40%, and Grinberg’s 3.13 fibo ran about 50% slower free-threaded. Those figures do not describe 3.14.

Memory is the other bill. PEP 779 reports a 15–20% higher geo-mean on pyperformance. Object headers are larger (None is 32 bytes versus 16 on AMD64).

Should you switch?

On r/learnpython, u/latkde (Apr 2026) answers “should I use free threading” with no, except for CPU-bound multithreaded programs.

u/marr75 (Nov 2025) noted the other side: by count of programs, most Python has no parallel code. Compute already lives in torch, BLAS, DuckDB, or a web server.

The benches hold for CPU-bound independent Python. Use the workload as the filter, not the headline.

Native libraries that already release the GIL (Polars, NumPy, PyTorch, hashlib-in-C) do not need 3.14t to use extra cores inside one call. Multiprocessing still wins when you want crash isolation, a separate address space, or extensions that have not shipped t wheels.

Nathan Goldbaum reported on 11 Mar 2026 that 180 of the 360 most-downloaded packages with compiled wheels ship t wheels. Quansight’s guide tracks which packages are ready. The June 2025 Summit figure of about one in six is stale.

Where Free-Threading Still Loses

Unmarked extensions put the lock back

The live blocker for free-threading is wheels, not the interpreter flag. Import an unmarked C extension and the process re-enables the GIL. Check sys._is_gil_enabled() after the import graph settles.

Shared mutation can lose

Contended mutation of shared Python objects reduced speedups or reversed them in arXiv 2603.04782. Free-threaded dict, list, and set locks behave like the old GIL for those types only. The thread-safety docs spell out which operations still need your own locking.

On r/programming, u/frenchtoaster (Oct 2025) flagged the NumPy case: mutating an object concurrently from two threads is unsafe C. Fine for a local script. Unsafe for a web service that assumed the GIL’s accidental atomicity.

Two builds, for now

Default 3.14 and 3.14t are both real. Pablo Galindo Salgado argued in July 2023 against a permanent GIL / no-GIL split. Phase III has not landed, so you are still choosing a binary, an ABI, and a wheel tag.

Frequently Asked Questions

Related Articles