Skip to content
← All Python articles

Python explainer

Free-Threaded Python in Plain Words

About 4 min to read. Allow extra time to work through examples.

See what the free-threaded build of Python changes, how to check which build you are running, and why shared data needs locks even more there.

This article uses Python. You can make it your practice mode to browse related content across the site.

The GIL is the reason a thread pool cannot speed up CPU-bound pure-Python code. Since Python 3.13 there is a second build of CPython that has no GIL, the free-threaded build. On this build, threads can run Python code on several cores at the same time. The thread pool from the first article in this track can then split the work across cores, as the process pool does.

Check which build you are running

import sys
import sysconfig

print(sysconfig.get_config_var("Py_GIL_DISABLED"))  # 1 on a free-threaded build
print(sys._is_gil_enabled())                        # False while the GIL is off

The two lines answer different questions. The first says how Python was built. The second says whether the GIL is on right now. A free-threaded build turns the GIL back on when you import an extension module that has not declared that it can run without the GIL. Python prints a warning when that happens.

sys._is_gil_enabled() starts with an underscore, but the free-threading documentation uses it for exactly this check. Python 3.13 added it.

Get the free-threaded build

It is not the default install. The python.org installers for macOS and Windows offer it as an option, and uv python install 3.14t installs it. Its executable usually has a t on the end, as in python3.14t.

Python 3.13 marked the build as experimental. Python 3.14 made it officially supported, though it is still optional.

Time the thread pool again

Run the timing script from the first article in this track with the free-threaded build:

uv run --python 3.14t speedup.py

The thread pool's time should drop toward the process pool's, because the four threads now run on four cores. The plain loop can be a little slower than on the default build, because the free-threaded build does extra work to keep objects consistent when threads run at the same time.

Shared data needs locks even more

The GIL hid some races by making thread switches in the middle of an update rare. Without the GIL, threads run at the same moment, so a race that almost never showed up now shows up often. The page-view counter from the concurrency track, which loses updates without a lock, loses them more often.

The rules from the concurrency track stay the same:

  • Protect each shared value with one lock, and hold it for the whole read, change and write.
  • Share less. Give each thread its own data, and combine the results at the end.
  • Built-in types such as list and dict stay safe to use from several threads, so a single append or a single key update does not corrupt them. A check followed by an update still needs your own lock.

The next exercise uses one lock per key, so that two threads working on different keys do not wait for each other.

Check your understanding Your threaded code passes its tests on the default build and fails them on the free-threaded build. What is the likely cause? Show answer

The likely cause is a race that was there all along. On the default build, the GIL made a thread switch in the middle of an update rare, so the bug almost never showed. Without the GIL, threads run at the same moment, and the lost update happens often. Find the shared data, and put a lock around each update.

Going further

The free-threading how-to covers which extension modules support the build and how to keep the GIL off when an old extension would turn it back on.

step 23 of 24

Parallel Python with Process Pools

Up next Build a Thread-Safe Cache with One Lock per Key