skip to content

What does `concurrent.interpreters.create()` return, and where does that interpreter run?

level: juniorimportance: nice to knowfreq 18%

answer

  1. Not a process, not a thread
  2. Same PID, different Python state
  3. sys.modules is not shared
  4. Handle object, driven with exec
  5. New in 3.14, PEP 734

basics

~20 s

It returns an Interpreter object: an additional Python interpreter living inside the same OS process, not a new process. It has its own sys.modules, its own module globals and its own GIL, and you drive it with Interpreter.exec().

solid answer

~40 s

`concurrent.interpreters.create()` (new in Python 3.14, PEP 734) returns an `Interpreter` object representing a fresh Python runtime **inside the current process** — `os.getpid()` is identical on both sides. What it does not share is Python-level state: it gets its own `sys.modules`, its own copy of every module's globals and builtins, and since 3.12 its own GIL. You run code in it with `Interpreter.exec(code)`, which executes a source string in that interpreter **in the calling thread** and blocks until it finishes; `Interpreter.call(fn, *args)` runs a callable instead, `Interpreter.call_in_thread(fn)` runs one on a new `threading.Thread`, and `Interpreter.prepare_main(**names)` seeds names into its `__main__`. If the code raises, `exec()` re-raises it in the caller as `concurrent.interpreters.ExecutionFailed`. `Interpreter.close()` tears it down.

code

python · 15 lines
python
from concurrent import interpreters
import os

interp = interpreters.create()
results = interpreters.create_queue()
interp.prepare_main(results=results)

interp.exec("import os; results.put(os.getpid())")
print("same process:", results.get() == os.getpid())

interp.exec("import sys; results.put('json' in sys.modules)")
import json
print("shares main's sys.modules:", results.get())

interp.close()

go deeper

for a junior

Remember the one-line shape: same process, separate Python state, created with concurrent.interpreters.create() and driven with Interpreter.exec(). Being able to say it is neither a thread nor a process is enough at this level.

for a middle

Be ready to list exactly what is isolated (sys.modules, module globals, builtins, the GIL) versus what is shared (PID, file descriptors, environment), and to explain that exec() blocks in the calling thread.

for a senior

An interviewer expects you to place this against threads and processes in a real design, and to flag the operational consequences: no fault isolation, per-interpreter import cost, and a hard limit on which objects can cross the boundary.

for a principal

Own the adoption question. Multiple interpreters are new stdlib surface with real ecosystem constraints, so the tradeoff you argue is maturity and dependency compatibility against the process-per-worker memory and startup bill you are paying today.

### The one-sentence model `concurrent.interpreters.create()` gives you a second Python runtime that lives inside the process you are already running. It is not a subprocess, not a thread, and not a sandbox. It is a complete, independent copy of the interpreter's Python-level state, sitting next to the one you started with. ### Process, thread, interpreter — three different things A **process** is an OS-level container: its own address space, its own PID, its own copy of everything. A **thread** is a schedulable flow of execution inside a process; threads in one process share all memory. An **interpreter**, in CPython's sense, is a bundle of Python-level runtime state: the `sys.modules` mapping, the import machinery's caches, the builtins, the set of loaded module objects with their globals, the interned strings, the exception state and (since Python 3.12, PEP 684) its own GIL. So `create()` sits between the other two. It shares the process — same PID, same file descriptors, same environment variables, same heap, same current working directory — but not the Python state layered on top of it. Setting `os.environ["REGION"]` in the main interpreter *is* visible in a new one, because that is process state. Importing `json` in the main interpreter is *not* visible, because `sys.modules` is per-interpreter. ### Driving it The `Interpreter` object is a handle, not a running thing. Nothing executes until you ask: - `Interpreter.exec(code)` compiles and runs a **source string** in that interpreter's `__main__`. Crucially, it runs on the OS thread that called it and blocks until it returns — `exec()` by itself buys you no concurrency at all. - `Interpreter.call(callable, *args, **kwargs)` runs a callable there and returns its result, provided the callable, the arguments and the result can all cross the interpreter boundary. - `Interpreter.call_in_thread(callable, ...)` starts a `threading.Thread` that does the same thing, and hands you the `Thread`. This is where actual parallelism starts: two interpreters, two threads, two GILs. - `Interpreter.prepare_main(**names)` binds names into the target's `__main__` before you run anything — the usual way to hand it a queue. - `Interpreter.close()` destroys it. `Interpreter.id` is its numeric identity and `Interpreter.is_running()` reports whether code is executing in it. The module also offers `get_current()`, `get_main()` and `list_all()` for introspection. ### Errors cross as `ExecutionFailed` An exception raised inside the other interpreter cannot simply propagate — the exception object belongs to a different runtime with different class objects. So `exec()` raises `concurrent.interpreters.ExecutionFailed` (a subclass of `InterpreterError`) in the caller, carrying a rendered traceback of the original failure in its message. You will not be catching the original `ValueError` on the outside; plan error handling around that. ```python from concurrent import interpreters interp = interpreters.create() try: interp.exec("raise ValueError('bad row')") except interpreters.ExecutionFailed: print("the other interpreter failed") ``` ### Why it exists CPython has had multiple interpreters in its C API for decades, and the private plumbing was reachable from Python only through undocumented internals. Two things made it a real feature. PEP 684 (Python 3.12) moved the GIL from a single process-wide lock to one lock per interpreter, so two interpreters can genuinely execute Python bytecode at the same time on two cores. PEP 734 (Python 3.14) then shipped the public `concurrent.interpreters` module and, alongside it, `concurrent.futures.InterpreterPoolExecutor`, so the capability is usable without touching C. The result is a third point on the concurrency map: parallelism like processes, but without process creation, without a separate address space, and with a cheaper handoff for the data types that can be shared. The costs are the flip side of the isolation — each interpreter re-runs its own imports, module-level state is not shared, and only certain objects can move between them. ### What it is not Three corrections worth having ready. It is **not a sandbox**: the new interpreter runs with the same privileges, the same file descriptors and the same environment as the one that created it, and a segmentation fault in native code inside it kills the whole process. It is **not free**: creating one costs single-digit milliseconds, and because its `sys.modules` starts nearly empty, it re-runs every import your code touches there. And on its own it is **not concurrent**: `create()` starts nothing, and `exec()` blocks the caller, so parallelism only appears when you pair each interpreter with a thread — which is exactly what `concurrent.futures.InterpreterPoolExecutor` does for you. ### What to say in an interview Same process, separate Python state, own GIL; created with `create()`, driven with `exec()`/`call()`, seeded with `prepare_main()`, torn down with `close()`; new in 3.14 on top of the 3.12 per-interpreter GIL. That is the whole shape.

  • Does `Interpreter.exec()` give you concurrency on its own?
    No. `Interpreter.exec()` runs the code on the OS thread that called it and blocks until it finishes, so on its own it is strictly serial. Parallelism needs a thread per interpreter: either drive each one from your own `threading.Thread`, use `Interpreter.call_in_thread()`, or let `concurrent.futures.InterpreterPoolExecutor` manage the worker threads and their interpreters for you.
  • If interpreters do not share `sys.modules`, what do they still share?
    Everything that belongs to the process rather than to Python: the PID, open file descriptors, the environment (`os.environ` changes are visible across interpreters), the current working directory, signal disposition and the C-library globals a native extension may keep. That is also why a crash or a `SIGSEGV` in one interpreter takes the whole process down — isolation here is Python-level, not fault isolation.
  • How does an exception raised inside another interpreter reach the caller?
    It cannot be re-raised as itself — the exception class belongs to a different runtime — so `Interpreter.exec()` raises `concurrent.interpreters.ExecutionFailed` in the calling interpreter, a subclass of `InterpreterError`, whose message carries the rendered traceback from the far side. Code that wants to branch on the original exception type has to encode it explicitly, for example by returning a status through a queue.

Two tenants in one building: they share the address, the plumbing and the front door (the process), but each has their own furniture and their own keys (their own modules and globals).

saying these in an interview costs you the question

  • Calling it a subprocess or saying it forks a new process
  • Claiming interpreters share sys.modules or module globals
  • Thinking Interpreter.exec() runs the code concurrently
  • Expecting the original exception type to propagate to the caller
  • Believing an interpreter is a security sandbox
  • Saying multiple interpreters have existed in the stdlib for years

context