Python · Python Threads and Processes

Python: Free-Threaded Python and Subinterpreters

The build without a GIL - its status in 3.13 and 3.14, the races it exposes and the C extensions that re-enable the GIL - and subinterpreters with their own GILs through InterpreterPoolExecutor.

  • 6 min read
  • 11 practice questions
On this page
  1. Description
  2. The free-threaded build
  3. What stays safe, and what breaks
  4. C extensions can bring the GIL back
  5. Subinterpreters
  6. Choosing, as of Python 3.14
  7. Common mistakes
  8. Check yourself
  9. Key takeaways
  10. Quick revise
  11. Practice

Description

For thirty years, the GIL meant one thing for Python programmers: threads can't compute in parallel, so CPU-heavy work goes to processes. Python 3.13 and 3.14 changed that picture with two new options. The free-threaded build is a version of Python with no GIL at all, where threads really do run on several cores at once. Subinterpreters let one process run several separate Python interpreters side by side, each with its own GIL. This lesson explains both: what they give you, what they cost, and the old bugs they can wake up.

Here's the kind of bug we mean. A service has counted requests like this for years:

served = 0

def handle(request):
    global served
    served += 1
    ...

It never lost a count in production. Then the team runs its CPU-heavy endpoints on the free-threaded build, and the totals stop adding up. Nothing in the code changed - so what did? By the end of the first section you'll know.

We'll go from simple to more technical:

  1. The free-threaded build: what it is, how to tell which build you're on, and its status.
  2. What stays safe without the GIL, and what breaks.
  3. C extensions and the GIL coming back.
  4. Subinterpreters and InterpreterPoolExecutor.
  5. Choosing between threads, subinterpreters and processes.

The free-threaded build

PEP 703 made the GIL optional when Python is built. The result is a separate interpreter, usually installed as python3.14t (the t is for "threaded"), next to the normal python3.14:

  • In Python 3.13 it was experimental.
  • In Python 3.14 it's officially supported (PEP 779), and much faster than in 3.13 - the release notes put the single-threaded slowdown at about 5-10%.
  • It's not the default. Plain python3 still has the GIL, and you can't switch the GIL off while a normal build is running.

To check which one you're on:

import sys, sysconfig

sys._is_gil_enabled()                          # True on the normal build
sysconfig.get_config_var("Py_GIL_DISABLED")    # 1 if Python was built free-threaded, 0 if not

On the free-threaded build, CPU-bound threads finally run in parallel: four threads crunching numbers can use four cores, with no processes and no pickling.

What stays safe, and what breaks

Without the GIL, Python still protects its built-in objects from being corrupted. Lists, dicts and sets lock themselves internally, so items.append(x) or cache[key] = value from many threads at once still leaves a valid list or dict.

What the free-threaded build doesn't protect - and the normal build never did either - is a sequence of steps. served += 1 is three steps: read served, add one, write it back. On the normal build, the GIL almost never switches threads in the middle of such a short sequence, so the race stays hidden for years. Without the GIL, two threads really do run the same line at the same moment, both read 41, both write 42, and a count is lost.

That's the answer to the puzzle: the bug was always there. The GIL just made it rare. The fix is the same as in the threading skill:

lock = threading.Lock()

def handle(request):
    global served
    with lock:
        served += 1

Before moving code to the free-threaded build, look for every read-then-write and check-then-act on shared data, and lock it.

C extensions can bring the GIL back

Libraries written in C - NumPy, database drivers, image libraries - were written assuming the GIL exists. Each one must declare that it's safe without it. If the free-threaded build imports a C extension that hasn't, it turns the GIL back on for the whole process and prints a RuntimeWarning. Your code keeps working, but the parallel speed-up silently disappears. (-X gil=0 or PYTHON_GIL=0 forces the GIL off anyway, at your own risk.)

This is the main reason the free-threaded build isn't the default yet: the ecosystem is still being updated. Check that your dependencies publish free-threaded wheels (their file names include cp314t) before planning around it.

Subinterpreters

The second option works on the normal build. One process can run several interpreters, each with its own modules, its own global variables and - since Python 3.12 - its own GIL. Python 3.14 added an easy way to use them:

from concurrent.futures import InterpreterPoolExecutor

with InterpreterPoolExecutor(max_workers=4) as pool:   # like ThreadPoolExecutor, but each worker is an interpreter
    totals = list(pool.map(score, batches))

What to know:

  • They run Python code in parallel. Measured on an 8-core laptop, four CPU-bound tasks took 0.49 s on four threads, 0.12 s on four subinterpreters, and 0.14 s on four processes.
  • They share no objects. Arguments and results are copied between interpreters, much like with processes. The function you submit must be one that can be found by name (defined at the top level of a module), or a self-contained function that uses no global names.
  • Each worker keeps its own globals between tasks. A function that incremented a module-level counter returned [1, 2, 3] over three tasks on a one-worker pool - while the main interpreter's counter stayed 0.
  • They start faster than processes - a pool's first task took about 24 ms, against about 57 ms for a process pool with spawn - and live in one process.
  • C extensions must support them too, and many can't yet be imported into a subinterpreter.

There's also a lower-level module, concurrent.interpreters, for creating interpreters by hand and passing data through queues.

Choosing, as of Python 3.14

The workNormal buildFree-threaded build
Lots of waiting on I/Othreads or asynciothreads or asyncio
CPU-heavy pure Pythonprocesses, or InterpreterPoolExecutorthreads
CPU-heavy NumPy and similarthreads (their C code releases the GIL)threads
Must survive a crash in a C libraryprocessesprocesses

Both new options are young. For most services today, processes remain the safe default for CPU-heavy Python; try the new options where their extensions are supported and measure.

Common mistakes

MistakeWhat happensDo this instead
Thinking Python 3.13/3.14 removed the GILexpecting speed-ups that don't comethe free-threaded build is separate: python3.14t
Moving unlocked += or check-then-act code to free-threadinglost updates, double worklock every compound step on shared data
Ignoring the RuntimeWarning about the GILthe GIL is back on; no speed-upuse extensions that support free-threading
Treating subinterpreters like threadsthey don't see the main interpreter's objectspass data in, return results
Submitting a function that uses globals but isn't importableNotShareableErrordefine it at module level

Check yourself

Try to answer before opening each one. Recalling beats re-reading.

1. Why did the request counter start losing counts on the free-threaded build?

Show answer

served += 1 is a read, an add and a write. On the normal build, the GIL almost never switches threads in the middle, so the race was hidden. Without the GIL, two threads can do it at the same moment and one update is lost. Put a lock around it.

2. Is the GIL gone in Python 3.14?

Show answer

No. The normal build still has it. A separate free-threaded build without a GIL is officially supported in 3.14, but it's not the default.

3. What does InterpreterPoolExecutor give you on the normal build that ThreadPoolExecutor doesn't?

Show answer

Each worker is a separate interpreter with its own GIL, so CPU-heavy Python runs in parallel - at the cost of sharing no objects with the main interpreter.

Key takeaways

  • The free-threaded build (python3.14t) has no GIL; it's supported in 3.14 but not the default. Check with sys._is_gil_enabled().
  • Built-in objects stay internally safe without the GIL; your multi-step operations don't - lock them.
  • One C extension that doesn't support free-threading turns the GIL back on.
  • Subinterpreters run in parallel on the normal build, share no objects, and keep their own globals.
  • Processes remain the safest choice for CPU-heavy work until your dependencies support the new options.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool (Python 3.14)

The workNormal buildFree-threaded build
Lots of waiting on I/Othreads or asynciothreads or asyncio
CPU-heavy pure Pythonprocesses, or InterpreterPoolExecutorthreads
CPU-heavy NumPy and similarthreadsthreads
Must survive a crash in a C libraryprocessesprocesses

Snippets worth knowing by heart

import sys, sysconfig
sys._is_gil_enabled()                          # False only when running without a GIL
sysconfig.get_config_var("Py_GIL_DISABLED")    # 1 on a free-threaded build

with lock:                                     # still needed without the GIL
    served += 1

from concurrent.futures import InterpreterPoolExecutor
with InterpreterPoolExecutor(max_workers=4) as pool:
    totals = list(pool.map(score, batches))    # score: a module-level function

The traps

  • "3.14 removed the GIL" - only the separate python3.14t build has none, and it's not the default.
  • Unlocked += and check-then-act - hidden by the GIL, exposed without it.
  • A C extension without free-threading support - turns the GIL back on (RuntimeWarning).
  • Subinterpreters see none of your objects - pass data in, return results.
  • Lambdas or closures using globals in an interpreter pool - NotShareableError.
  • Globals in a worker interpreter persist between its tasks, separate from the main interpreter's.

Staff-level points, one line each

  • Replacing the GIL: biased reference counting, immortal (3.12) and deferred objects, mimalloc, per-object critical sections, QSBR.
  • 3.14 re-enabled the specializing interpreter in the free-threaded build - single-threaded cost fell to ~5-10%.
  • PEP 779: experimental (3.13) → supported, opt-in (3.14) → default (no date).
  • Contention on shared objects (refcounts, locks) can make more threads slower - partition data per thread.
  • Extensions declare support with Py_mod_gil; free-threaded wheels are tagged cp314t.
  • Per-interpreter GIL since 3.12 (PEP 684); concurrent.interpreters and InterpreterPoolExecutor in 3.14 (PEP 734).
  • Objects never cross interpreters: some immutables cross natively, the rest are pickled - a queued list arrives equal but not identical.
  • Functions cross by name (module-level) or as code (self-contained, no globals or closures).
  • Measured: interpreter creation ~4 ms; first task ~24 ms vs ~57 ms for a spawn process pool; 4 CPU tasks 0.12 s vs 0.49 s on threads.
  • Each interpreter imports its own copy of modules; extensions need multi-phase init to load in one.

Say it in an interview

  • "On the normal build I'd use processes, or an interpreter pool if our extensions support it."
  • "Before moving to free-threading, I'd audit every read-modify-write on shared state and lock it."
  • "If threads got slower without the GIL, I'd look for contention on shared objects."
  • "Subinterpreters copy data, so I'd send small inputs and return small results."

Practice

This skill comes with 11 multiple-choice questions that tell you which misconception a wrong answer reveals. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Python: Free-Threaded Python and Subinterpreters free

Practice Python: Free-Threaded Python and Subinterpreters free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime