Python 3.13 Free-Threading and JIT: What Changed and Should You Care

Python 3.13 ships an optional build without the GIL and an experimental JIT. Learn what each one does today, how to try them, and whether to act now.

Executive Summary: Python 3.13 adds an optional free-threaded build without the GIL and an experimental JIT that is off by default. This post explains what each changes, why free-threading costs single-threaded speed and lacks package support, and recommends upgrading to the standard 3.13 build while testing the free-threaded one without deploying it.

For more than twenty years, one sentence has been true of CPython: only one thread runs Python code at a time. Python 3.13.0, released on 7 October 2024, ships the first official builds in which that sentence can be false. You can download an interpreter today, start four threads on a CPU-bound function, and watch four cores work.

Python 3.13 free-threading refers to a separate build of the interpreter, defined by PEP 703, with the global interpreter lock (GIL) disabled. The GIL is a lock inside CPython that lets one thread at a time execute bytecode. For background, see how the GIL shapes threads and processes. The JIT, defined by PEP 744, is a just-in-time compiler that turns hot bytecode into machine code while the program runs.

This article uses Python 3.13.0. To follow the experiments, you need both the standard build and the free-threaded build, which installs as python3.13t. Everything shown uses the standard library.

My position: these two features are the most important change to CPython in a decade, and almost nobody should put them in production this year. Understand them, measure them, and prepare your code. Do not bet a service on something the core team itself labels experimental.

What Python 3.13 changes at a glance

Feature Reference Status in 3.13 Should you act?
Free-threaded build PEP 703 Experimental, separate executable Test your code and dependencies on it
JIT compiler PEP 744 Experimental, off by default, needs a special build No action. Watch future releases
New interactive REPL Release notes Default Enjoy it
Color tracebacks and better error messages Release notes Default Nothing to do
Defined locals() semantics PEP 667 Default Matters for debuggers and unusual code
copy.replace(), warnings.deprecated Standard library, PEP 702 Default Use when convenient
Nineteen legacy modules removed PEP 594 Removed Search your code before upgrading

The myth: Python 3.13 removed the GIL

Headlines say that Python 3.13 has no GIL. If you install Python 3.13 the usual way and run python3.13, the GIL is there, exactly as before. Nothing about your threads changes.

The GIL is absent only in a different executable, built with a special option and usually named python3.13t. It is a separate install with its own packages. The release notes call it experimental, and the standard build remains the default and the recommendation. So the accurate statement is this: Python 3.13 lets you opt in to an interpreter without the GIL.

Try Python 3.13 free-threading

Install the free-threaded build

  • Windows and macOS. The python.org installers include an optional checkbox for free-threaded binaries. It is off by default.
  • Linux. Several distributions and package archives publish it as a separate package. You can also build from source with ./configure --disable-gil.

Then confirm what you are running. Never assume.

python3.13t -VV
Python 3.13.0 experimental free-threading build (main, Oct  7 2024, 23:49:14) [GCC 13.2.0]
python3.13t -c "import sys, sysconfig; print(sysconfig.get_config_var('Py_GIL_DISABLED'), sys._is_gil_enabled())"
1 False

The first value says the build supports running without the GIL. The second says whether the GIL is active right now. Those can differ, and the next section explains why that matters.

Measure it

Save this complete script as threads_bench.py. It runs a CPU-bound function four times in sequence, then on four threads.

import sys
import sysconfig
import time
from concurrent.futures import ThreadPoolExecutor

WORKERS = 4
N = 5_000_000


def cpu_task(n: int) -> int:
    total = 0
    for i in range(n):
        total += i * i
    return total


def main() -> None:
    free_threaded = bool(sysconfig.get_config_var('Py_GIL_DISABLED'))
    gil_enabled = sys._is_gil_enabled() if hasattr(sys, '_is_gil_enabled') else True
    print(f'free-threaded build: {free_threaded}, GIL enabled: {gil_enabled}')

    start = time.perf_counter()
    for _ in range(WORKERS):
        cpu_task(N)
    sequential = time.perf_counter() - start

    start = time.perf_counter()
    with ThreadPoolExecutor(max_workers=WORKERS) as pool:
        list(pool.map(cpu_task, [N] * WORKERS))
    threaded = time.perf_counter() - start

    print(f'sequential: {sequential:.2f}s')
    print(f'{WORKERS} threads:  {threaded:.2f}s  ({sequential / threaded:.1f}x)')


if __name__ == '__main__':
    main()
python3.13 threads_bench.py
python3.13t threads_bench.py

This is my own illustrative run on an 8-core laptop. Reproduce it with the two commands above, because your numbers will differ.

free-threaded build: False, GIL enabled: True
sequential: 1.20s
4 threads:  1.23s  (1.0x)

free-threaded build: True, GIL enabled: False
sequential: 1.71s
4 threads:  0.47s  (3.6x)

Read both halves. On the standard build, four threads give no speedup, as always. On the free-threaded build, the same threads finish 3.6 times faster than the sequential run. That is the feature working.

Now compare the two sequential lines. The free-threaded build takes 1.71 seconds for work that the standard build does in 1.20. Single-threaded code got slower. That is the price.

What free-threading costs

Single-threaded speed

Removing one big lock means protecting many small things individually. Reference counts must be safe across threads, and containers need internal locking. The official free-threading guide states that the 3.13 build has substantial single-threaded overhead, about 40 percent on the pyperformance benchmark suite.

Much of that comes from one decision. The specializing interpreter introduced in Python 3.11, which is responsible for most of the speed gains of recent releases, is disabled in the free-threaded build for now, because it is not yet thread-safe. The core team has said it expects to reduce this overhead in later releases.

The break-even arithmetic

Here is the calculation that tells you whether free-threading helps your workload. Two effects combine. Each thread runs about 1.4 times slower, and only the parallel part of your program scales. With a parallel fraction p and n threads, the speedup over the standard build is roughly:

speedup = 1 / ((1 - p) + p / n) / 1.4
Parallel share of the work 2 threads 4 threads 8 threads
100 percent 1.4x 2.9x 5.7x
90 percent 1.3x 2.2x 3.4x
50 percent 1.0x 1.1x 1.3x
0 percent (single-threaded) 0.7x 0.7x 0.7x

The table is my own arithmetic from the 40 percent figure, so treat it as a planning aid, not a benchmark. Its message is clear all the same. A program that is fully parallel across many cores wins big. A program that is half serial barely breaks even with four threads. A single-threaded program simply gets slower. Most web request handlers fall in the last two rows, since they wait on I/O and do little CPU work.

Extension modules can switch the GIL back on

This is the detail that silently invalidates benchmarks. C extension modules must declare that they are safe without the GIL. If you import one that has not, the free-threaded interpreter re-enables the GIL for the whole process and prints a warning:

RuntimeWarning: The global interpreter lock (GIL) has been enabled to load module 'somepackage',
which has not declared that it can run safely without the GIL. To override this behavior and keep
the GIL disabled (at your own risk), run with PYTHON_GIL=0 or -Xgil=0.

From that moment, you are running the slower free-threaded build with the GIL on: the worst of both. The warning is easy to miss in a log. Therefore, my rule for any experiment is to print sys._is_gil_enabled() after all imports, as the benchmark script does, and to trust no measurement without it.

You can force the GIL off with the environment variable PYTHON_GIL=0, at your own risk. An extension that is not thread-safe may then crash or corrupt data. Free-threaded wheels carry the tag cp313t. If a package you need has no such wheel, pip tries to compile it from source, which often fails.

Your own code needs locks

Built-in types such as list and dict protect their own internals in the free-threaded build, so the interpreter will not crash. Your program’s logic is another matter. Any read-modify-write on shared data can interleave.

import threading

counter = 0
lock = threading.Lock()


def unsafe(n: int) -> None:
    global counter
    for _ in range(n):
        counter += 1                 # read, add, write: three steps


def safe(n: int) -> None:
    global counter
    for _ in range(n):
        with lock:
            counter += 1


def run(target) -> int:
    global counter
    counter = 0
    threads = [threading.Thread(target=target, args=(200_000,)) for _ in range(4)]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()
    return counter


if __name__ == '__main__':
    print('unsafe:', run(unsafe))
    print('safe:  ', run(safe))

The correct total is 800,000. On python3.13t, the unsafe version prints a smaller number that changes on every run, and the safe version prints 800,000. On the standard build, the unsafe version often happens to produce the right total, which has hidden this class of bug for years. Free-threading does not create these races. It exposes them, and it makes them frequent.

The JIT: real, experimental, and not yet faster

A just-in-time compiler translates frequently executed code into machine code at runtime. CPython’s new JIT uses a technique called copy-and-patch: prebuilt machine-code templates are stitched together for hot sequences of bytecode. It builds on the specializing interpreter from 3.11 and the internal optimizer added since.

Three facts keep expectations realistic.

  • It is off by default. You get it only from a build configured with --enable-experimental-jit. The standard installers do not enable it.
  • It needs LLVM at build time, although not at runtime.
  • It is not yet a speed-up. The release notes describe the current performance gains as modest. PEP 744 presents this release as the foundation, with improvements expected in later versions.

So the JIT in 3.13 matters as a direction, not as a feature you can use. There is nothing to configure and nothing to rewrite. If someone tells you that Python 3.13 is fast because it has a JIT, they have read a headline and not the release notes.

The parts of 3.13 you will actually use

A much better REPL

The interactive prompt has been rebuilt. It now supports multi-line editing, so pressing the up arrow recalls a whole function, not one line. It has color, and typing exit or quit works without parentheses. Press F1 for help, F2 for history, and F3 for paste mode, which accepts large blocks of code without mangling indentation. If a tool misbehaves with it, set PYTHON_BASIC_REPL=1 to get the old prompt.

Clearer errors

Tracebacks are in color by default in a terminal. Two new messages address mistakes that waste hours. The first catches a script that is named like a standard library module:

AttributeError: module 'random' has no attribute 'randint' (consider renaming '/home/dev/random.py'
since it has the same name as the standard library module named 'random' and prevents importing
that standard library module)

The second suggests the right keyword argument:

>>> 'a b c'.split(max_split=1)
TypeError: split() got an unexpected keyword argument 'max_split'. Did you mean 'maxsplit'?

Small additions

import copy
import warnings
from dataclasses import dataclass


@dataclass(frozen=True)
class Point:
    x: int
    y: int


@warnings.deprecated('Use distance_from_origin() instead')
def magnitude(point: Point) -> float:
    return (point.x ** 2 + point.y ** 2) ** 0.5


if __name__ == '__main__':
    moved = copy.replace(Point(1, 2), y=5)
    print(moved)
    print(magnitude(moved))
Point(x=1, y=5)
DeprecationWarning: Use distance_from_origin() instead
5.0990195135927845

copy.replace() creates a modified copy of an immutable object, including dataclasses and named tuples. warnings.deprecated (PEP 702) marks an API as deprecated for both the runtime and type checkers. The exact position of the warning line in your terminal may differ, since it goes to standard error.

What was removed

Python 3.13 completes the removal of the “dead batteries” announced in PEP 594. Nineteen modules are gone, including cgi, cgitb, crypt, imghdr, telnetlib, pipes, nntplib, and audioop. The 2to3 tool and lib2to3 were removed as well. These modules have raised deprecation warnings since 3.11, so nobody was surprised, yet old code still breaks.

Search before you upgrade:

grep -rnE "^(import|from) (cgi|cgitb|crypt|imghdr|telnetlib|pipes|nntplib|audioop|aifc|sunau|sndhdr|uu|xdrlib|chunk|mailcap|msilib|nis|ossaudiodev|spwd|lib2to3)\b" src/ tests/

Any match needs a replacement, usually a maintained package from PyPI. The symptom at runtime is ModuleNotFoundError, often inside an old dependency. The removals in the previous release are covered in these Python 3.12 upgrade notes, and they still apply if you are jumping two versions.

How to upgrade to the standard build

  1. Install 3.13 beside your current version and check it with python3.13 --version.
  2. Create a fresh virtual environment and install your dependencies.
python3.13 -m venv .venv313
source .venv313/bin/activate          # macOS and Linux
.venv313\Scripts\Activate.ps1         # Windows PowerShell
python -m pip install -r requirements.txt
  1. Read the install log. A Building wheel for ... line means the package has no 3.13 wheel yet.
  2. Run the removal search shown above on your own code.
  3. Run the tests with warnings as errors: python -W error -m pytest.
  4. Add 3.13 to CI beside your current version, and move production once the first bugfix release is out and the tests are green.

How real teams are approaching 3.13

  • Standard build first. Application teams upgrade to regular 3.13 on their normal schedule, for the REPL, the error messages, and the longer support window.
  • A free-threaded CI job that may fail. Library maintainers add 3.13t to their test matrix as a non-blocking job, to find thread-safety bugs early.
  • Extension authors do the heavy lifting. Projects with C, Cython, or Rust code are auditing global state and publishing cp313t wheels. Application developers mostly wait for them.
  • Benchmarks include the GIL check. Serious measurements record whether the GIL was actually disabled, because an import can turn it back on.
  • Processes remain the production answer. For CPU-bound parallel work today, teams still use multiple processes.

In my experience trying a data-processing job on the free-threaded build during the release week, the first benchmark showed no improvement at all. The job imported a compiled dependency that had not been updated, so the interpreter had quietly re-enabled the GIL, and I had missed the warning in a long log. I was measuring the slower build with none of its benefit. Once I printed sys._is_gil_enabled() at start-up, the cause was obvious, and I have added that line to every experiment since.

Should you care? A decision framework

  1. Do you maintain a library, especially one with compiled code? Yes, care now. Test on 3.13t, fix thread-safety issues, and plan free-threaded wheels. Your users cannot move until you do.
  2. Do you run CPU-bound work that is hard to split across processes? Experiment now. Large shared in-memory data is the case where threads beat processes.
  3. Is your service I/O-bound? Upgrade to the standard build when convenient. Free-threading offers you little, and it costs single-threaded speed.
  4. Do you depend on many compiled packages? Wait. Check for cp313t wheels again in a few months.
  5. Are you hoping the JIT makes your code faster? Not in this release. Nothing to do.

When NOT to use the free-threaded build

  • In production, this year. The build is explicitly experimental. Bugs in it, or in an extension running without the GIL, can corrupt data in ways that are very hard to diagnose.
  • For single-threaded or I/O-bound programs. You pay roughly 40 percent in speed and gain nothing.
  • When any important dependency lacks support. One unsupported extension re-enables the GIL, or forces you to run it unsafely.

Common mistakes

  • Assuming python3.13 has no GIL. The standard executable is unchanged. Only python3.13t can run without it.
  • Benchmarking without checking the GIL state. An extension import can re-enable it. You then report numbers for a configuration you did not intend.
  • Forcing PYTHON_GIL=0 to silence the warning. Extensions that are not thread-safe can crash or silently corrupt memory.
  • Treating the GIL as your lock. Code that relied on it for safety now has real data races. Shared state needs explicit locks.
  • Expecting the JIT to speed things up. It is disabled in standard builds, and its gains are modest so far.
  • Skipping the removal check. A forgotten import cgi or import imghdr fails at runtime, often in a rarely used code path.

Key takeaways

  • The standard Python 3.13 build still has the GIL.
  • The free-threaded build, python3.13t, is a separate, experimental install defined by PEP 703.
  • It scales CPU-bound threads across cores, and it runs single-threaded code about 40 percent slower in this release.
  • An unsupported extension module switches the GIL back on, so check sys._is_gil_enabled().
  • Without the GIL, your own shared state needs locks.
  • The JIT (PEP 744) is experimental, off by default, and not yet a meaningful speed-up.
  • Upgrade to standard 3.13 for the REPL and error messages, after checking for the nineteen removed modules.

FAQ

Does Python 3.13 remove the GIL?

Not by default. The standard build keeps the GIL. Python 3.13 adds a separate, experimental free-threaded build, usually installed as python3.13t, in which the GIL is disabled.

What is free-threaded Python?

It is a build of CPython without the global interpreter lock, defined by PEP 703. Threads in it can execute Python bytecode on several CPU cores at the same time.

Is free-threaded Python slower?

For single-threaded code, yes. The official documentation reports about 40 percent overhead on the pyperformance suite in 3.13. Multi-threaded CPU-bound code can be much faster, because it uses several cores.

Does Python 3.13 have a JIT compiler?

Yes, an experimental one defined by PEP 744. It is disabled by default, requires a specially configured build, and currently provides only modest performance gains.

Should I use Python 3.13 free-threading in production?

Not yet. It is experimental, slower for single-threaded code, and unsupported by many extension packages. Test your code on it, and deploy the standard build.

Upgrade the interpreter, experiment with the future

Python 3.13 is two releases in one. The standard build is a safe, pleasant upgrade with a better prompt and clearer errors. The free-threaded build and the JIT are previews of where CPython is heading, published so that the ecosystem can prepare. Use the first, and study the second.

Rule of thumb: run the standard build in production, run the free-threaded build in CI, and print the GIL state before you believe any benchmark.

Share this article

Leave a Reply

Your email address will not be published. Required fields are marked *