Alex Vakhitov

software engineering5 min read

The GIL and JIT compilation in Python

By Alex Vakhitov

Night motorway with light trails heading towards a city skyline

Updated September 2026: corrected the history of the GIL and added the Python 3.13 and 3.14 changes.

Two terms come up again and again when people talk about Python concurrency and performance: the GIL (Global Interpreter Lock) and JIT (just-in-time) compilation. They're different things, but both have a big effect on how programs behave and perform. This post covers what each one is, how it works, how they differ, a little history, and what has changed in CPython recently.

What is the GIL?

History

The Global Interpreter Lock was Python's answer to the problems of multi-threading in its reference implementation, CPython. Guido van Rossum added it in 1992, when he added thread support to the interpreter, so it was already there long before Python 1.5.

How it works

The GIL is a mutex that lets only one thread execute Python bytecode at a time in a process. It protects the interpreter's own internals, such as reference counts, so that CPython and C extensions don't have to lock every object. It doesn't make your code thread-safe: an operation like counter += 1 is still several bytecode steps, and threads can be switched between them. Shared state still needs a lock:

import sys
import threading

counter = 0
lock = threading.Lock()

def work(n):
    global counter
    for _ in range(n):
        with lock:  # the GIL alone doesn't make += atomic
            counter += 1

threads = [threading.Thread(target=work, args=(100_000,)) for _ in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print(counter)
print("GIL enabled:", sys._is_gil_enabled())

On a standard build this prints 400000 and GIL enabled: True. (Tested with Python 3.13 and 3.14. sys._is_gil_enabled() was added in 3.13.)

The cost of the GIL is that CPU-bound Python code can't run on several cores at once through threads. Programs that mostly wait on I/O suffer much less, because blocking I/O calls, and many C extensions, release the GIL while they work.

Foundations

The GIL is a form of mutual exclusion. Edsger Dijkstra set out the mutual exclusion problem in 1965, and locks, semaphores and other synchronisation primitives are the standard ways to formalise and implement it.

What's changed: free-threaded Python

PEP 703, accepted in 2023, set out a plan to make the GIL optional in three phases:

  • Python 3.13 (October 2024) shipped an experimental free-threaded build, usually installed as python3.13t, that runs without the GIL.
  • Python 3.14 (October 2025) made the free-threaded build officially supported, though still optional and separate from the default build (PEP 779). The Python documentation puts its cost for single-threaded code at roughly 5 to 10%. If you import a C extension that hasn't declared it's safe without the GIL, the interpreter turns the GIL back on; you can check with sys._is_gil_enabled() and override it with the PYTHON_GIL environment variable or the -X gil option.
  • There's no date yet for making the free-threaded build the default.

Running the example above on a 3.14 free-threaded build prints GIL enabled: False, and the lock is what keeps the count right. Python 3.14 also added concurrent.interpreters (PEP 734), which runs several interpreters in one process as another route to parallelism.

As of September 2026, Python 3.15 is due on 1 October 2026. Its release candidates add a stable ABI for free-threaded builds (PEP 803), which should make it easier for extension authors to support them.

What is JIT compilation?

History

The idea of compiling code at runtime goes back a long way. John McCarthy's work on LISP around 1960 is often cited as an early example, and research on Smalltalk and Self developed it further. Java's HotSpot virtual machine made it mainstream in the late 1990s, and since then JIT compilers have become standard in many language runtimes, including JavaScript engines. In the Python world, PyPy has had a tracing JIT for many years.

How it works

A JIT compiler translates code that would otherwise be interpreted into native machine code while the program runs, usually for the parts that run most often. The aim is to speed things up using information that's only available at runtime, such as the actual types flowing through a function.

Foundations

JIT compilers use optimisation techniques grounded in graph theory and formal languages, such as data-flow analysis on control-flow graphs. Many of these optimisations, like constant folding and loop unrolling, aren't specific to JITs; ahead-of-time compilers use them too. What a JIT adds is the ability to specialise code based on how it actually behaves at runtime.

What's changed: a JIT in CPython

  • Python 3.11 introduced the specialising adaptive interpreter (PEP 659), which rewrites bytecode on the fly into faster versions specialised for the types it sees. It isn't a JIT, but it laid the groundwork.
  • Python 3.13 added an experimental copy-and-patch JIT compiler (PEP 744). It was only available if you built CPython from source with --enable-experimental-jit.
  • Python 3.14 includes the JIT in the official Windows and macOS binaries, but it's still experimental and off by default. You can turn it on with the PYTHON_JIT=1 environment variable, and check its status through sys._jit. The free-threaded build also now uses the specialising adaptive interpreter.
  • As of September 2026, the Python 3.15 release candidates include an upgraded JIT with reported average speed-ups of around 8 to 9% on x86-64 Linux and 12 to 13% on AArch64 macOS, but it remains opt-in.

Differences between the GIL and JIT

  1. Scope: the GIL is specific to CPython (and a few other interpreters) and mainly affects multi-threaded programs. JIT compilation is a general optimisation technique used in many languages.
  2. Performance: the GIL limits CPU-bound multi-threaded performance. A JIT usually improves performance by optimising code at runtime.
  3. Complexity: the GIL keeps the interpreter and C extensions simpler at the cost of parallelism, but it doesn't make user code thread-safe. A JIT adds a compilation step and complexity to the runtime, in return for faster execution.

Summary

The GIL and JIT compilation solve different problems. The GIL simplifies the interpreter's own memory management, and JIT compilation speeds up code at runtime. Both are changing in CPython: the GIL is becoming optional, and a JIT is arriving step by step. Understanding them helps with the trade-offs in language design and runtime behaviour, and with choosing between threads, processes and interpreters in your own programs.