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 FeatureThreads (threading)Processes (multiprocessing)Subinterpreters (PEP 684)
GIL ScopeShared Process GILIsolated per ProcessPer-Interpreter GIL
Multi-Core Parallel CPU❌ No (GIL Blocked)βœ… Yesβœ… Yes
Memory IsolationShared HeapFully IsolatedFully Isolated
Startup CostLow (~8KB Stack)Heavy (~15MB-30MB RAM)Lightweight (~Sub-MB)
IPC MechanismDirect PointersOS Pipes / PicklingIsolated Channels / Memory
Display Options
Appearance
Text Size
100%