May 8, 2024·6 min read·engineeringpythoncpythonconcurrencyjit

Python 3.13 beta separates free threading from JIT compilation

Python 3.13's first beta exposes two different runtime experiments. Free threading changes concurrent execution and extension requirements, while the JIT changes how hot interpreter traces execute.

Python's new runtime experiments address different limits. Removing the global interpreter lock allows Python threads to execute concurrently across processor cores. Compiling hot interpreter traces aims to reduce the work required to execute Python operations. The first Python 3.13 beta, released May 8, includes both experiments, but neither turns into a default property of every Python installation.

The distinction is important because the two changes have different compatibility requirements. A free-threaded interpreter changes assumptions that native extensions may make about shared state. A JIT changes the execution path inside the interpreter. Treating both as a single performance feature obscures what each build is intended to exercise and what application developers can infer from its behavior.

I choose Python for the capability available through its libraries, including libraries whose important work happens in native code. That makes the extension boundary central to this release. More parallel Python execution is useful only when the packages needed by an application can participate safely. The beta provides a concrete interpreter against which that question can be investigated before the final release.

Beta establishes a compatibility target

The release announcement describes the beta phase as the opportunity for third-party projects to prepare support. Python 3.13 is planned to be feature complete at this point, while modifications and occasional removals remain possible before release candidates. It is a preview release and is not recommended for production. This combination gives package maintainers a useful target without claiming that every interface has become final.

The documentation included in the beta tag is especially useful for separating the available build choices. It documents free threading through --disable-gil and the JIT through --enable-experimental-jit. These are interpreter configuration options, so an application's ordinary dependency declaration does not silently enable them. The selected interpreter build is part of the environment being exercised.

This creates several distinct meanings of compatibility. A library can work under ordinary Python 3.13 while its native extension still needs changes for a free-threaded build. A Python program can retain its semantics under a JIT-enabled build without becoming faster. Reporting that a project supports 3.13 therefore needs enough detail to identify which interpreter configuration that statement covers.

A free-threaded build can still have the GIL enabled

The beta documentation distinguishes whether an interpreter was built to support free threading from whether the current process has the GIL enabled. Build configuration is available through sysconfig. A separate query reports the runtime state. Both can be inspected with the documented interfaces:

import sys
import sysconfig

print(sysconfig.get_config_var("Py_GIL_DISABLED"))
print(sys._is_gil_enabled())

This example is for the Python 3.13 beta, including its internal runtime query. The first result describes a configuration choice. The second describes what the running interpreter is doing. Those values answer different questions, which is why checking only the executable's name or the intention behind its build is insufficient when diagnosing concurrent behavior.

The beta permits running a free-threaded build with the GIL enabled through PYTHON_GIL or the -X gil command-line option. It also describes a compatibility mechanism for native extensions. Importing an extension that has not indicated free-threading support can enable the GIL unless the user explicitly forced it off. An environment can consequently start with a free-threading-capable interpreter and acquire a different execution mode when it imports an extension.

That behavior gives extension authors a transition mechanism. Existing code is not automatically treated as safe merely because it can be loaded. The extension must declare whether it supports the changed execution model. This makes the imports used by an application relevant to understanding its effective runtime configuration, including imports that happen indirectly through another package.

Extension support involves a separate build

The beta's release documentation says C API extensions need builds specifically for the free-threaded interpreter. Modules using multiple-phase initialization declare support through Py_mod_gil. Modules using single-phase initialization have PyUnstable_Module_SetGIL. Those mechanisms communicate an extension's support to CPython. They do not perform the work of making its internal state safe under concurrent execution.

The broader motivation appears in PEP 703. It explains that the GIL can constrain Python coordination even when expensive numerical computation happens in native kernels. Moving work into separate processes introduces communication and state-sharing costs. The proposal therefore addresses how programs express parallel work, including the Python operations that connect their native computations.

That is a more useful description than assuming every scientific workload spends all its time in Python bytecode. An application can delegate large computations while still doing enough Python-level coordination to encounter a global execution bottleneck. Conversely, removing that bottleneck does not remove serialization that the application's own algorithm requires. The build expands what threads can do, while the program still determines which work can proceed independently.

The beta documentation also warns about bugs and substantial single-threaded overhead in the experimental build. That is part of the current implementation state. Parallel execution has additional synchronization requirements, and an application that does not expose useful parallel work may receive those costs without a compensating benefit. The ordinary build remains a separate configuration for that reason.

The JIT compiles optimized traces

The beta's JIT documentation starts with specialized Tier 1 bytecode. When execution becomes hot enough, CPython translates it to an internal Tier 2 representation made of micro-operations. Optimization passes operate on that representation. The JIT then turns the optimized result into machine code. There is also a Tier 2 interpreter intended mainly for debugging earlier stages of the pipeline.

This architecture allows different parts of the experiment to be exercised separately. Configuring --enable-experimental-jit=interpreter enables the Tier 2 interpreter without machine-code generation. The ordinary --enable-experimental-jit setting includes the JIT, while yes-off builds it but leaves it disabled until enabled at runtime. PYTHON_JIT controls runtime activation for the appropriate build. A binary built without the machinery cannot acquire it merely through an environment variable.

The code-generation technique is copy-and-patch, explained in PEP 744. LLVM supplies machine-code fragments during the build. Runtime compilation assembles those fragments and patches trace-specific information, avoiding a runtime LLVM dependency. The beta describes performance gains as modest and the implementation as experimental. It provides a place for further optimization work without promising that this build improves every program.

I regard these configuration distinctions as necessary information for anyone interpreting results from the beta. A failure in the Tier 2 interpreter asks a different debugging question from a failure that appears only after machine-code generation. The documented modes allow contributors to narrow that question. They also prevent a report about one experiment from becoming an unsupported claim about the other.

Ordinary compatibility work remains substantial

Frame access changes independently of the experimental build options. The beta notes describe frame.f_locals inside a function as a write-through proxy to the frame's local variables, replacing the dictionary interface previously returned there. That makes an interface used by tools part of the compatibility work. A debugger manipulating local state has a different concern from an application merely calling a function. Both execute on the same interpreter, but they depend on different parts of its behavior.

This is another reason to describe support through the operations a package actually uses. A library consisting of ordinary Python functions and a tool inspecting live interpreter frames do not exercise identical contracts. Native extension imports add another set of requirements. A useful beta report identifies the failing operation and the selected build configuration, allowing the interpreter maintainers to distinguish a general language regression from a problem in one experimental execution path. The release's request for third-party participation is valuable because those distinct uses exist outside the interpreter's own source tree.

Runtime experiments are not the only reason to exercise 3.13. The beta also removes modules deprecated through PEP 594, including cgi, telnetlib, and imghdr. The proposal explains the maintenance cost of keeping obsolete or superseded modules in the standard library. Their removal can affect an application using the ordinary interpreter, without any participation in free threading or the JIT.

The beta's interactive interpreter is another independent change. On supported Unix-like systems, it adds multiline editing and separate help and history interfaces, with a paste mode for larger code blocks. PYTHON_BASIC_REPL selects the older interface when needed. These changes affect interactive work, while module removals affect imports and runtime experiments affect execution. Keeping those categories distinct gives third-party maintainers a clearer account of what their 3.13 support needs to cover during the beta period.