Skip to main content

Modules/_interpretersmodule.c

_interpretersmodule.c is the C backing for the interpreters standard-library module introduced by PEP 734 (Python 3.13). It lets Python code create and manage sub-interpreters at runtime: each interpreter gets its own PyInterpreterState, its own import state, and (in the free-threaded 3.13+ build) its own GIL. The module is intentionally low-level; the interpreters package in Lib/ wraps it with a friendlier class-based API.

The file owns one per-module C struct, _interpreters_module_state, that caches frequently used type objects so that the method implementations do not have to re-lookup them on every call.

Map

LinesSymbolRole
1-50includes, _interpreters_module_stateModule state struct: cached PyTypeObject * for InterpreterID and exception types
51-110_get_module_state, _get_current_module_stateHelpers to retrieve _interpreters_module_state from a module or from the calling interpreter
111-180InterpreterID typePython-visible opaque handle wrapping a int64_t interpreter ID; supports ==, hash, repr
181-240_interp_from_idResolves an InterpreterID or plain int to a live PyInterpreterState *; raises InterpreterNotFoundError on miss
241-310interp_create / _interpreters_createCalls Py_NewInterpreter; returns an InterpreterID; accepts isolated=True keyword to configure sharing policy
311-370interp_destroy / _interpreters_destroyCalls Py_EndInterpreter after verifying the target is not the calling interpreter
371-430interp_list_all / _interpreters_list_allWalks the _PyRuntime.interpreters.head linked list and returns a list[InterpreterID]
431-480interp_get_current / _interpreters_get_currentWraps PyInterpreterState_Get() in an InterpreterID
481-560interp_get_main / _interpreters_get_mainReturns the ID of the main interpreter (_PyRuntime.interpreters.main)
561-650interp_run_string / _interpreters_run_stringMarshals script str into the target interpreter and executes it via _PyInterpreterState_SetRunningMain + PyRun_SimpleStringFlags
651-740interp_run_func / _interpreters_run_funcSerialises a callable via marshal + pickle, sends the bytes cross-interpreter, and reconstructs + calls it on the other side
741-780module_exec, module_free, _PyInterpreters_moduleModule slot init: registers InterpreterID type and the four exception subclasses
781-800PyInit__interpretersStandard PyModuleDef_Init entry point

Reading: module state and type caching

Rather than storing types in module-level globals, the file uses Python's per-module state slot. module_exec populates the struct once at import time:

// Modules/_interpretersmodule.c:741
static int
module_exec(PyObject *mod)
{
_interpreters_module_state *state = get_module_state(mod);

/* Cache the InterpreterID type for fast allocation in interp_create. */
state->PyInterpreterID_Type = (PyTypeObject *)
PyType_FromModuleAndSpec(mod, &InterpreterID_spec, NULL);
if (state->PyInterpreterID_Type == NULL) return -1;
if (PyModule_AddType(mod, state->PyInterpreterID_Type) < 0) return -1;

/* Register InterpreterNotFoundError as a module attribute. */
state->interp_not_found_error = PyErr_NewExceptionWithDoc(
"interpreters.InterpreterNotFoundError",
"Raised when an interpreter ID does not match any live interpreter.",
PyExc_RuntimeError, NULL);
if (!state->interp_not_found_error) return -1;
return PyModule_AddObjectRef(mod, "InterpreterNotFoundError",
state->interp_not_found_error);
}

Reading: creating a sub-interpreter

interp_create is the workhorse. It calls into the runtime's interpreter lifecycle API, configures isolation, and wraps the resulting ID:

// Modules/_interpretersmodule.c:241
static PyObject *
interp_create(PyObject *self, PyObject *args, PyObject *kwds)
{
static char *kwlist[] = {"isolated", NULL};
int isolated = 1;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|p:create", kwlist,
&isolated))
return NULL;

PyThreadState *tstate = PyThreadState_New(
isolated ? _PyInterpreterConfig_InitIsolatedConfig()
: _PyInterpreterConfig_InitSharedConfig());
if (tstate == NULL) return NULL;

int64_t id = PyInterpreterState_GetID(tstate->interp);
PyThreadState_Clear(tstate);
PyThreadState_Delete(tstate);
return _PyInterpreterID_New(id);
}

The isolated=True default means the sub-interpreter does not share the main interpreter's sys.modules cache or codec registry, which is the behaviour described in PEP 734.

Reading: running a string in another interpreter

interp_run_string must execute Python source in a foreign PyInterpreterState. It does this by temporarily switching the calling OS thread into the target interpreter:

// Modules/_interpretersmodule.c:561
static PyObject *
interp_run_string(PyObject *self, PyObject *args)
{
PyObject *id_obj, *script;
if (!PyArg_ParseTuple(args, "OU:run_string", &id_obj, &script)) return NULL;

PyInterpreterState *interp = _interp_from_id(self, id_obj);
if (interp == NULL) return NULL;

const char *src = PyUnicode_AsUTF8(script);
if (src == NULL) return NULL;

/* Switch this OS thread into the target interpreter. */
PyThreadState *new_ts = PyThreadState_New(interp);
PyThreadState *save = PyThreadState_Swap(new_ts);

int rc = PyRun_SimpleString(src);

PyThreadState_Swap(save);
PyThreadState_Clear(new_ts);
PyThreadState_Delete(new_ts);

if (rc != 0) {
/* Exception was printed; raise a generic RunFailedError here. */
PyErr_SetString(run_failed_error(self), "interpreter run failed");
return NULL;
}
Py_RETURN_NONE;
}

Port status

Not yet ported to gopy. gopy currently has a single interpreter model. Porting this module requires first implementing PyInterpreterState isolation in the gopy runtime, which is tracked as part of the free-threading roadmap.