Python Module 14 – Concurrency and parallelism: threads, processes, interpreters
Running Python without the GIL (3.14t builds)
Install and detect a free-threaded CPython 3.14t build, measure what the GIL costs your threads, and keep code correct when they run on several cores.
What you will learn
- Install a free-threaded build of Python 3.14 and detect which build is running
- Measure how many cores CPU-bound threads keep busy while the GIL is on
- Check whether a setting or an extension module turned the GIL back on
- Write thread code that stays correct with the GIL and without it
Before you start
What you’ll learn · Before you start
What you will learn
- Install a free-threaded build of Python 3.14 and detect which build is running
- Measure how many cores CPU-bound threads keep busy while the GIL is on
- Check whether a setting or an extension module turned the GIL back on
- Write thread code that stays correct with the GIL and without it
Before you start
On this page
A free-threaded build of CPython is a separate interpreter, installed as python3.14t beside the usual
python3.14, in which the global interpreter lock can be switched off. Without the GIL, the threads of one process
run Python code on several cores at the same moment, so CPU-bound work can speed up with threads instead of
processes. Python 3.14 is the first release in which this build is officially supported rather than experimental
(PEP 779), and it is still optional: the default build keeps its GIL. Three costs come with it. Single-threaded code
runs a little slower. An extension module that has not declared support for running without the GIL turns it back on
for the whole process. And code that relied on the GIL to make several steps happen together needs locks of its own,
on either build.
Two builds of Python 3.14
The default build and the free-threaded build of Python 3.14
Text description of the diagram
The diagram has two boxes, one above the other, joined by an arrow that says the free-threaded build is a separate interpreter.
- The top box is the default build, run as python3.14. Its four threads take turns holding one GIL, so only one of them runs Python code at any moment.
- The bottom box is the free-threaded build, run as python3.14t. Its four threads run Python code at the same time, each on its own core, because there is no GIL.
- Below that, still in the bottom box: this lasts until the program imports an extension module that has not declared it can run without the GIL. Python then turns the GIL back on for the whole process and prints a warning.
| Criterion | Default build | Free-threaded build |
|---|---|---|
| Command | python3.14 | python3.14t |
| Threads run Python code in parallel | no: one GIL | yes, while the GIL is off |
| Is the GIL on? | always | only if something turned it on |
| Context of a new thread | empty | a copy of its creator's |
| Extension modules | all | those built for it; others turn the GIL on |
| When to choose | Most programs today | CPU-bound threads whose extensions support it |
The two builds share the language and the standard library, but not compiled extension modules: a package with C
code needs a wheel built for the free-threaded build. PEP 703 marks such builds with the letter t in their ABI tag,
which is where sys.abiflags and the t in python3.14t come from. What’s New in Python 3.14 puts the cost at roughly
5 to 10 percent on single-threaded code, and the free-threading guide, measured with the pyperformance suite, at about
1 percent on macOS on Apple silicon to 8 percent on x86-64 Linux.
Which build am I running?
This file reports both things that matter: how the interpreter was built, and whether the GIL is on at this moment. It runs in your browser too, where it describes the browser’s default build:
"""Which build of CPython runs this file, and is the GIL on right now?"""
import sys
import sysconfig
print("free-threaded build:", sysconfig.get_config_var("Py_GIL_DISABLED") == 1)
print("'free-threading build' in sys.version:", "free-threading build" in sys.version)
print("ABI flags:", repr(getattr(sys, "abiflags", ""))) # contains 't' on a free-threaded build (Linux, macOS)
print("GIL enabled right now:", sys._is_gil_enabled())
print("new threads inherit the caller's context:", bool(sys.flags.thread_inherit_context))
print("catch_warnings() uses a context variable:", bool(sys.flags.context_aware_warnings)) Output
free-threaded build: False 'free-threading build' in sys.version: False ABI flags: '' GIL enabled right now: True new threads inherit the caller's context: False catch_warnings() uses a context variable: False
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 which_build.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
sysconfig.get_config_var("Py_GIL_DISABLED") == 1 is the check the free-threading guide recommends for decisions
about the build, and "free-threading build" in sys.version says the same in words. sys._is_gil_enabled() answers a
different question, whether the GIL is on right now, which on a free-threaded build can change while the program
runs. The last two lines are defaults that the free-threaded build changes, explained further down.
To get the free-threaded build on Windows, run py install 3.14t with the Python install manager. On macOS, the
python.org installer has a Free-threaded Python package under Customize, which adds python3.14t. On Linux, use a
distribution package if yours has one, or build CPython from source with ./configure --disable-gil.
What the GIL costs threads, measured
The same CPU-bound work, 32 pieces of one loop, runs on eight threads and then on eight processes. Each piece measures its own CPU time, so the output can say how many cores each pool kept busy at once:
"""The same CPU-bound work on 8 threads and on 8 processes: the processes show how many cores the computer
could give, the threads how many the GIL allowed them to use."""
import sys
import sysconfig
import time
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
WORKERS = 8
PIECES = 32
SIZE = 1_200_000 # loop iterations per piece
def work(n):
"""A pure-Python loop; returns the CPU time it took, measured in the worker that ran it."""
began = time.thread_time()
total = 0
for i in range(n):
total += i * i % 7
return time.thread_time() - began
def cores_busy(label, executor_class):
with executor_class(max_workers=WORKERS) as pool:
list(pool.map(work, [1] * WORKERS)) # start every worker before the clock starts
began = time.perf_counter()
cpu = sum(pool.map(work, [SIZE] * PIECES))
wall = time.perf_counter() - began
print(f"{label}: {cpu:.2f} s of CPU in {wall:.2f} s, cores busy at once: {cpu / wall:.1f}")
return cpu / wall
if __name__ == "__main__":
free = sysconfig.get_config_var("Py_GIL_DISABLED") == 1
print(f"free-threaded build: {free}, GIL enabled: {sys._is_gil_enabled()}")
threads = cores_busy("8 threads ", ThreadPoolExecutor)
processes = cores_busy("8 processes", ProcessPoolExecutor)
if threads < 1.2:
print("verdict: the threads stayed within one core; the processes show there were more to use")
else:
print("verdict: the threads ran Python code on several cores at once, as the processes did") Output
free-threaded build: False, GIL enabled: True 8 threads : 2.50 s of CPU in 3.04 s, cores busy at once: 0.8 8 processes: 2.37 s of CPU in 1.05 s, cores busy at once: 2.3 verdict: the threads stayed within one core; the processes show there were more to use
This output changes from run to run: the times depend on the computer and on what else it runs at the same moment; on the default build the verdict does not
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 threads_or_processes.py
Both pools did the same CPU work, about 2.4 seconds of it. The processes show how many cores the computer could give
at that moment, here 2.3 on a computer busy with other programs, and finished in 1.05 seconds. The threads needed 3.04
seconds and kept 0.8 cores busy, below one, because with the GIL on only one of them runs Python code at a time,
however many cores are idle. Run the same file with python3.14t
and the threads’ figure rises to meet the processes’, since each thread then runs on a core of its own.
What changes for your code
Built-in types such as dict, list and set protect each of their own operations with internal locks, so one
append() or one d[key] = value is as safe as with the GIL. Anything made of several steps is not: a check followed
by an update, counter += 1, moving an item between two lists. Those already needed a lock on the default build, and
without the GIL their races happen far more often. The free-threading guide also warns that sharing one iterator
between threads is not safe, and that a frame’s local variables must not be read from another thread while it runs.
A few switches and habits decide how a program behaves on each build. PYTHON_GIL=1 or -X gil=1 turns the GIL on
in a free-threaded build, and PYTHON_GIL=0 or -X gil=0 keeps it off. The flag thread_inherit_context gives each
new thread a copy of its creator’s context. A start-up check tells a program whether its threads will run in
parallel. And a lock around next() makes one iterator safe to share.
Check yourself
Four questions on the free-threaded build
4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
The GIL switches, their failure on the default build and the changed defaults, reproduced Pro: opens with a Pro, Premium or Ultimate pass
Every switch that controls the GIL is run in this part, including the one that stops the default build before it starts, along with the changed default for new threads and a start-up check you can copy.
In this part:
- Asking the default build to drop the GIL: the fatal error, reproduced
- The context a new thread starts with, shown with decimal
- A start-up check that says whether threads will run in parallel
- Memory on the free-threaded build: what grows, and why
3 runnable examples (Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Code that is correct on both builds: iterators, locks and what they cost, measured Pro: opens with a Pro, Premium or Ultimate pass
Removing the GIL changes how often a race happens, not whether the code has one. This part reproduces a race that the default build shows too, fixes it with a lock, measures the lock, and ends with an exercise that is checked on both builds.
In this part:
- One generator shared by two threads: the ValueError, reproduced
- One lock around next(): every item exactly once, and what the lock costs
- What the built-in types guarantee, and what still needs your lock
- Measuring a switch to the free-threaded build, three runs
- Exercise: a counter and a parallel sum that are correct on both builds
- A counter and a parallel sum that are correct on both builds
2 runnable examples (Python) · 1 exercise with sample tests · about 5 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
-
Mid to senior · Warm-up
What is the difference between sys._is_gil_enabled() and the Py_GIL_DISABLED configuration variable, and when do you use each?
They answer two different questions. The configuration variable Py_GIL_DISABLED, read with sysconfig.get_config_var(), describes how the interpreter was built: it is 1 for a free-threaded build such as python3.14t and 0 for the default build, and it never changes while the program runs. The free-threading guide recommends it for decisions about the build, such as which wheels or code paths to use. sys._is_gil_enabled() reports whether the GIL is on in this process right now. On the default build it is always True; on a free-threaded build it is False unless PYTHON_GIL=1, -X gil=1 or the import of an extension module that has not declared support turned the GIL back on. Check it after the program's imports, when you need to know whether threads will really run in parallel.
-
Senior to staff
A service was moved to python3.14t so that its 16 worker threads compute in parallel, but its CPU use never goes above one core, and a warning about the GIL appears once in the start-up logs. Diagnose it and describe the fix.
-
Senior to staff
After switching a billing service to python3.14t, invoice totals computed in worker threads round differently, although no code changed. The threads are started inside a decimal.localcontext() block. What changed, and how do you make the service behave the same on both builds?
Key takeaways
- The free-threaded build is a separate interpreter,
python3.14t, officially supported from Python 3.14 and optional. - Check the build with
sysconfig.get_config_var("Py_GIL_DISABLED")and the running state withsys._is_gil_enabled(). - With the GIL on, CPU-bound threads keep at most one core busy; without it, they use several.
- An extension module without declared support turns the GIL back on;
PYTHON_GILand-X giloverride it. - Built-in types protect single operations; anything made of several steps still needs your own lock.
References
- Python support for free threading (Python Software Foundation)
- What's New In Python 3.14: free-threaded mode improvements (Python Software Foundation)
- sys._is_gil_enabled() (Python Software Foundation)
- Command line and environment: -X gil and PYTHON_GIL (Python Software Foundation)
- Using Python on Windows: installing free-threaded binaries (Python Software Foundation)
- Using Python on macOS: installing free-threaded binaries (Python Software Foundation)
- PEP 703: Making the Global Interpreter Lock Optional in CPython (Python Software Foundation)
- PEP 779: Criteria for supported status for free-threaded Python (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress