Subinterpreters, Per-Interpreter GIL & PEP 554/684
Historically, CPython supported multiple embedded interpreter instances within a single OS process, but all subinterpreters shared a single Global Interpreter Lock (GIL). Introduced in Python 3.12 (PEP 684) and Python 3.13 (PEP 554), Subinterpreters with Per-Interpreter GILs allow running multiple isolated CPython interpreters inside a single OS processβeach with its own independent GIL executing parallel code on separate CPU cores!
This chapter details Subinterpreter architecture, Per-Interpreter GIL mechanics (PEP 684), _xxsubinterpreters API (PEP 554), state isolation, and zero-copy IPC channels.
1. Subinterpreter Architecture & Per-Interpreter GIL (PEP 684)
Prior to Python 3.12, the GIL was a global process-wide lock (_PyRuntime.gil). PEP 684 refactored CPython internal state to move the GIL from PyRuntimeState into PyInterpreterState:
Per-Interpreter GIL Memory Architecture (Python 3.12+):
[ OS Process Memory Space (Single PID) ]
βββ Subinterpreter 1 (PyInterpreterState) ββ> Has GIL 1 ββ> [ CPU Core 1 ]
β βββ Isolated Heap, Modules, & Built-ins
βββ Subinterpreter 2 (PyInterpreterState) ββ> Has GIL 2 ββ> [ CPU Core 2 ]
β βββ Isolated Heap, Modules, & Built-ins
βββ Subinterpreter N (PyInterpreterState) ββ> Has GIL N ββ> [ CPU Core N ]
(Result: Multi-core CPU parallelism INSIDE A SINGLE PROCESS without process spawning overhead!)2. Subinterpreter State Isolation Invariants
Each subinterpreter is almost completely isolated from others in the same process:
- Isolated Heap & Objects: Python objects (
PyObject) created in Subinterpreter A cannot be directly referenced or accessed by Subinterpreter B! This prevents cross-interpreter reference count race conditions. - Isolated Modules (
sys.modules): Each subinterpreter imports its own independent copies of modules and global state. - Isolated GC: Each subinterpreter runs its own Generational Garbage Collector.
3. Subinterpreter API & Channels (_xxsubinterpreters / PEP 554)
PEP 554 exposes a standard library module (interpreters) for creating subinterpreters and passing data via Channels:
# Python 3.13+ Subinterpreter Channel Communication (PEP 554)
import _xxsubinterpreters as interpreters
# 1. Create a new subinterpreter (with its own independent GIL!)
interp_id = interpreters.create()
# 2. Create a cross-interpreter data channel
channel_id = interpreters.channel_create()
# 3. Run Python code inside Subinterpreter 2 in parallel!
code = f"""
import _xxsubinterpreters as interpreters
data = interpreters.channel_recv({channel_id})
print("Subinterpreter received:", data)
"""
interpreters.run_string(interp_id, code)
# 4. Send data over the channel
interpreters.channel_send(channel_id, "Hello from Main Interpreter!")4. Production Architectural Comparison: Subinterpreters vs. Processes vs. Threads
| Architecture Feature | Threads (threading) | Processes (multiprocessing) | Subinterpreters (PEP 684) |
|---|---|---|---|
| GIL Scope | Shared Process GIL | Isolated per Process | Per-Interpreter GIL |
| Multi-Core Parallel CPU | β No (GIL Blocked) | β Yes | β Yes |
| Memory Isolation | Shared Heap | Fully Isolated | Fully Isolated |
| Startup Cost | Low (~8KB Stack) | Heavy (~15MB-30MB RAM) | Lightweight (~Sub-MB) |
| IPC Mechanism | Direct Pointers | OS Pipes / Pickling | Isolated Channels / Memory |