Python Module 14 – Concurrency and parallelism: threads, processes, interpreters
Subinterpreters with concurrent.interpreters
Run isolated Python interpreters inside one process with concurrent.interpreters: create and call them, pass data through queues, and pool them.
What you will learn
- Create interpreters, run code in them and call functions in them
- Pass data between interpreters with calls, queues and memoryviews
- Run CPU-bound work in parallel on interpreters and measure what each one costs
- Write a fallback to threads for Pythons without concurrent.interpreters
Before you start
What you’ll learn · Before you start
What you will learn
- Create interpreters, run code in them and call functions in them
- Pass data between interpreters with calls, queues and memoryviews
- Run CPU-bound work in parallel on interpreters and measure what each one costs
- Write a fallback to threads for Pythons without concurrent.interpreters
Before you start
On this page
Since Python 3.14, the concurrent.interpreters module lets one process run several interpreters, each a complete
and isolated copy of the Python runtime with its own modules, its own global variables and its own GIL. Code in two
interpreters can therefore compute on two cores at the same moment, as two processes would, while everything stays
inside one process. Nothing is shared by default: arguments and results cross between interpreters as copies, cheap
ones for strings, numbers and tuples and pickled ones for most other objects, and a queue carries data from one
interpreter to another. An interpreter runs code only when a thread calls into it, so parallel work needs one thread
per interpreter, which InterpreterPoolExecutor arranges for you. The 3.14 release notes list the present limits:
slow start-up, extra memory for each interpreter, few ways to share objects, and many third-party extension modules
that do not support it yet.
One process, several interpreters
Several interpreters inside one process
Text description of the diagram
The diagram shows one process containing four boxes.
- The main interpreter runs your script as __main__, with its own modules and its own GIL.
- Interpreter 1 and interpreter 2 each have their own modules, their own global variables and their own GIL, so code in different interpreters can run on different cores at the same time when each runs in its own thread.
- An arrow from the main interpreter to interpreter 1 is labelled call(): the arguments go in and the result comes back as copies, not as shared objects.
- Interpreter 2 puts items on a queue, and the main interpreter gets them from it. A queue copies most objects; a few, such as a memoryview, share their data.
Each box in the diagram is a separate world. Importing a module in interpreter 1 does not import it in interpreter 2,
and a change to sys.path or to a global variable in one is invisible in the others, because each has its own copy
of sys, of builtins and even of __main__. Since Python 3.12 each interpreter also has its own GIL, which is what
lets two of them compute at the same time. The isolation is the point of the design: two interpreters can only meet
where your code passes something between them. It is not a security boundary, though. They share one address space,
and the documentation warns against using them to keep code away from data it must not read.
Creating an interpreter and calling into it
interpreters.create() makes a new, idle interpreter. exec() runs source code in it, and call() runs a function in
it and returns the result; both run in the current thread and return when the code is done:
"""Create an interpreter, run code in it, call a function in it, and see what it keeps to itself."""
import sys
import textwrap
from concurrent import interpreters
sys.stdout.reconfigure(line_buffering=True) # every interpreter has its own sys.stdout: show each line at once
print(f"top of the file runs as {__name__}")
visits = []
SOURCE = textwrap.dedent("""
from concurrent import interpreters
print("exec() runs in", __name__, "of interpreter", interpreters.get_current().id, flush=True)
""")
def visit(who):
"""Record a visit in this interpreter's own copy of the list."""
visits.append(who)
return f"{who}: {len(visits)} visit(s) in this interpreter's list"
if __name__ == "__main__":
print(visit("main"))
interp = interpreters.create()
interp.exec(SOURCE)
print(interp.call(visit, "sub"))
print(interp.call(visit, "sub"))
print(visit("main"))
print("interpreters now:", [i.id for i in interpreters.list_all()])
interp.close()
print("after close():", [i.id for i in interpreters.list_all()]) Output
top of the file runs as __main__ main: 1 visit(s) in this interpreter's list exec() runs in __main__ of interpreter 1 top of the file runs as <fake __main__> sub: 1 visit(s) in this interpreter's list sub: 2 visit(s) in this interpreter's list main: 2 visit(s) in this interpreter's list interpreters now: [0, 1] after close(): [0]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 first_interpreter.py
The exec() code ran in the new interpreter’s own __main__, as interpreter 1. The first call() showed something
easy to miss: to run visit(), the new interpreter imported this script under the name <fake __main__>, so its top
line ran a second time there. That is why the code that creates interpreters sits under if __name__ == "__main__":,
exactly as with process pools. Each interpreter then counted visits in its own list: two in the main interpreter and
two in interpreter 1, never four in one place. An interpreter keeps its modules and variables between calls, so the
second call found the list that the first one had filled. close() destroyed the interpreter, leaving only the main
one, 0.
Threads, processes or interpreters
| Criterion | Threads | Processes | Interpreters |
|---|---|---|---|
| Python code on several cores | no: one GIL | yes | yes: a GIL each |
| Shared | every object | nothing | only queues and memoryviews |
| Arguments | passed as they are | pickled | copied, often pickled |
| Cost of one more | smallest | largest | in between |
| Extension modules | all | all | only compatible ones |
| When to choose | Waiting work, shared data | CPU work with any library | CPU work in one process |
Interpreters sit between threads and processes on every row. Several tools move data between them, each with one job.
A queue from create_queue() carries objects from one interpreter to another. A memoryview shares the bytes of a
buffer, so writes in one interpreter are seen in the other. prepare_main() binds values into an interpreter’s
__main__ before you run code there. call_in_thread() runs a call in a new thread of its own, which is how several
interpreters compute at once. An uncaught exception in another interpreter reaches you as ExecutionFailed. And
InterpreterPoolExecutor puts all of it behind the executor interface of concurrent.futures.
Where interpreters are not available, the module is simply missing. WebAssembly builds of Python, such as the one
behind the Run buttons of these lessons, have no concurrent.interpreters, and Python 3.13 and older have no public
module for it at all. Code that must run everywhere tries the import and falls back to threads or processes.
Check yourself
Four questions on interpreters
4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Moving data between interpreters, with each rule shown Pro: opens with a Pro, Premium or Ultimate pass
Interpreters meet only where your code passes something between them. This part runs each way across, the copies, the shared buffer, the queue and the exceptions, and shows what refuses to cross.
In this part:
- What a call copies, what it shares and what it refuses
- Results through a queue, from an interpreter in its own thread
- Errors across the boundary: ExecutionFailed and what it keeps
3 runnable examples (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interpreters in parallel: speed, start-up and memory, measured Pro: opens with a Pro, Premium or Ultimate pass
Interpreters compute in parallel inside one process, but each one costs time to create and memory to keep. This part measures both against threads and processes, and ends with code that works where interpreters are missing.
In this part:
- Eight interpreters on eight threads against eight threads in one interpreter
- What one more worker costs: thread, interpreter, process
- Which modules load in a subinterpreter: a check you can run on your own dependencies
- Exercise: count words on interpreters, or on threads where there are none
- Count words on interpreters, or on threads where there are none
3 runnable examples (Python) · 1 exercise with sample tests · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Mid to senior · Warm-up
What does it mean that interpreters created with concurrent.interpreters are isolated, and what is it not?
Each interpreter has its own copy of the runtime's state: its own imported modules, its own sys, builtins and __main__, its own global variables and, since Python 3.12, its own GIL. Importing a module or changing a global in one interpreter changes nothing in another, and objects do not cross between them: calls and queues copy most values, and only a few types, such as queues and memoryviews, share their data. That isolation is what lets two interpreters run Python code on two cores at once. It is not a security boundary: all interpreters live in one process and one address space, and an extension module can reach across, so the documentation warns against using them to keep code away from data it must not read.
-
Mid to senior
A service scores incoming documents with a pure-Python model that takes about 400 ms of CPU per document, using a ProcessPoolExecutor of 8 workers. A colleague proposes InterpreterPoolExecutor instead. How do you decide, and what would you measure first?
-
Mid to senior
After moving a batch job to InterpreterPoolExecutor, every task fails with an ImportError saying a module "does not support loading in subinterpreters", although the same import works in the main interpreter. Explain why, and what your options are.
Key takeaways
concurrent.interpreters, new in Python 3.14, runs isolated interpreters inside one process, each with its own modules, globals and GIL.exec()andcall()run in the calling thread;call_in_thread()orInterpreterPoolExecutorgives parallel work.- Arguments and results cross as copies; queues and memoryviews are the few things that are shared.
- Interpreters import your script again to find its functions, so creation code goes under the main guard.
- Their current limits are start-up time, memory per interpreter and extension modules that do not support them yet.
References
- concurrent.interpreters: multiple interpreters in the same process (Python Software Foundation)
- What's New In Python 3.14: multiple interpreters in the standard library (Python Software Foundation)
- concurrent.futures: InterpreterPoolExecutor (Python Software Foundation)
- PEP 734: Multiple Interpreters in the Stdlib (Python Software Foundation)
- PEP 684: A Per-Interpreter GIL (Python Software Foundation)
- resource: resource usage information (Python Software Foundation)
Report a problem with this lesson
Kept only in this browser. Your Learn progress