https://github.com/python/cpython/commit/04273adae0dcde7ae87ba06586b77290794f7f61
commit: 04273adae0dcde7ae87ba06586b77290794f7f61
branch: 3.14
author: Eric Snow <ericsnowcurren...@gmail.com>
committer: ericsnowcurrently <ericsnowcurren...@gmail.com>
date: 2025-06-12T08:19:26-06:00
summary:

[3.14] gh-134939: Add the concurrent.interpreters Module (gh-135414)

PEP-734 has been accepted (for 3.14).

(FTR, I'm opposed to putting this under the concurrent package, but
doing so is the SC condition under which the module can land in 3.14.)

(cherry picked from commit 62143736b, AKA gh-133958)

files:
A Doc/library/concurrent.interpreters.rst
A Lib/concurrent/interpreters/__init__.py
A Lib/concurrent/interpreters/_crossinterp.py
A Lib/concurrent/interpreters/_queues.py
A Lib/test/support/channels.py
A Misc/NEWS.d/next/Library/2025-05-30-09-46-21.gh-issue-134939.Pu3nnm.rst
D Lib/test/support/interpreters/__init__.py
D Lib/test/support/interpreters/_crossinterp.py
D Lib/test/support/interpreters/channels.py
D Lib/test/support/interpreters/queues.py
M .github/CODEOWNERS
M Doc/library/concurrency.rst
M Doc/library/concurrent.rst
M Doc/library/python.rst
M Doc/whatsnew/3.14.rst
M Lib/concurrent/futures/interpreter.py
M Lib/test/test__interpchannels.py
M Lib/test/test_concurrent_futures/test_interpreter_pool.py
M Lib/test/test_interpreters/test_api.py
M Lib/test/test_interpreters/test_channels.py
M Lib/test/test_interpreters/test_lifecycle.py
M Lib/test/test_interpreters/test_queues.py
M Lib/test/test_interpreters/test_stress.py
M Lib/test/test_interpreters/utils.py
M Lib/test/test_sys.py
M Lib/test/test_threading.py
M Lib/test/test_types.py
M Makefile.pre.in
M Modules/_interpchannelsmodule.c
M Modules/_interpqueuesmodule.c
M Modules/_testinternalcapi.c
M Python/crossinterp_exceptions.h

diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index c1de8a529a8db7..ea0de5e55e563c 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -281,9 +281,13 @@ Doc/howto/clinic.rst          @erlend-aasland
 # Subinterpreters
 **/*interpreteridobject.*     @ericsnowcurrently
 **/*crossinterp*              @ericsnowcurrently
-Lib/test/support/interpreters/  @ericsnowcurrently
 Modules/_interp*module.c      @ericsnowcurrently
+Lib/test/test__interp*.py     @ericsnowcurrently
+Lib/concurrent/interpreters/  @ericsnowcurrently
+Lib/test/support/channels.py  @ericsnowcurrently
+Doc/library/concurrent.interpreters.rst  @ericsnowcurrently
 Lib/test/test_interpreters/   @ericsnowcurrently
+Lib/concurrent/futures/interpreter.py  @ericsnowcurrently
 
 # Android
 **/*Android*                  @mhsmith @freakboy3742
diff --git a/Doc/library/concurrency.rst b/Doc/library/concurrency.rst
index 5be1a1106b09a0..18f9443cbfea20 100644
--- a/Doc/library/concurrency.rst
+++ b/Doc/library/concurrency.rst
@@ -18,6 +18,7 @@ multitasking). Here's an overview:
    multiprocessing.shared_memory.rst
    concurrent.rst
    concurrent.futures.rst
+   concurrent.interpreters.rst
    subprocess.rst
    sched.rst
    queue.rst
diff --git a/Doc/library/concurrent.interpreters.rst 
b/Doc/library/concurrent.interpreters.rst
new file mode 100644
index 00000000000000..8860418e87a585
--- /dev/null
+++ b/Doc/library/concurrent.interpreters.rst
@@ -0,0 +1,198 @@
+:mod:`!concurrent.interpreters` --- Multiple interpreters in the same process
+=============================================================================
+
+.. module:: concurrent.interpreters
+   :synopsis: Multiple interpreters in the same process
+
+.. moduleauthor:: Eric Snow <ericsnowcurren...@gmail.com>
+.. sectionauthor:: Eric Snow <ericsnowcurren...@gmail.com>
+
+.. versionadded:: 3.14
+
+**Source code:** :source:`Lib/concurrent/interpreters.py`
+
+--------------
+
+
+Introduction
+------------
+
+The :mod:`!concurrent.interpreters` module constructs higher-level
+interfaces on top of the lower level :mod:`!_interpreters` module.
+
+.. XXX Add references to the upcoming HOWTO docs in the seealso block.
+
+.. seealso::
+
+   :ref:`isolating-extensions-howto`
+       how to update an extension module to support multiple interpreters
+
+   :pep:`554`
+
+   :pep:`734`
+
+   :pep:`684`
+
+.. XXX Why do we disallow multiple interpreters on WASM?
+
+.. include:: ../includes/wasm-notavail.rst
+
+
+Key details
+-----------
+
+Before we dive into examples, there are a small number of details
+to keep in mind about using multiple interpreters:
+
+* isolated, by default
+* no implicit threads
+* not all PyPI packages support use in multiple interpreters yet
+
+.. XXX Are there other relevant details to list?
+
+In the context of multiple interpreters, "isolated" means that
+different interpreters do not share any state.  In practice, there is some
+process-global data they all share, but that is managed by the runtime.
+
+
+Reference
+---------
+
+This module defines the following functions:
+
+.. function:: list_all()
+
+   Return a :class:`list` of :class:`Interpreter` objects,
+   one for each existing interpreter.
+
+.. function:: get_current()
+
+   Return an :class:`Interpreter` object for the currently running
+   interpreter.
+
+.. function:: get_main()
+
+   Return an :class:`Interpreter` object for the main interpreter.
+
+.. function:: create()
+
+   Initialize a new (idle) Python interpreter
+   and return a :class:`Interpreter` object for it.
+
+
+Interpreter objects
+^^^^^^^^^^^^^^^^^^^
+
+.. class:: Interpreter(id)
+
+   A single interpreter in the current process.
+
+   Generally, :class:`Interpreter` shouldn't be called directly.
+   Instead, use :func:`create` or one of the other module functions.
+
+   .. attribute:: id
+
+      (read-only)
+
+      The interpreter's ID.
+
+   .. attribute:: whence
+
+      (read-only)
+
+      A string describing where the interpreter came from.
+
+   .. method:: is_running()
+
+      Return ``True`` if the interpreter is currently executing code
+      in its :mod:`!__main__` module and ``False`` otherwise.
+
+   .. method:: close()
+
+      Finalize and destroy the interpreter.
+
+   .. method:: prepare_main(ns=None, **kwargs)
+
+      Bind "shareable" objects in the interpreter's
+      :mod:`!__main__` module.
+
+   .. method:: exec(code, /, dedent=True)
+
+      Run the given source code in the interpreter (in the current thread).
+
+   .. method:: call(callable, /, *args, **kwargs)
+
+      Return the result of calling running the given function in the
+      interpreter (in the current thread).
+
+   .. method:: call_in_thread(callable, /, *args, **kwargs)
+
+      Run the given function in the interpreter (in a new thread).
+
+Exceptions
+^^^^^^^^^^
+
+.. exception:: InterpreterError
+
+   This exception, a subclass of :exc:`Exception`, is raised when
+   an interpreter-related error happens.
+
+.. exception:: InterpreterNotFoundError
+
+   This exception, a subclass of :exc:`InterpreterError`, is raised when
+   the targeted interpreter no longer exists.
+
+.. exception:: ExecutionFailed
+
+   This exception, a subclass of :exc:`InterpreterError`, is raised when
+   the running code raised an uncaught exception.
+
+   .. attribute:: excinfo
+
+      A basic snapshot of the exception raised in the other interpreter.
+
+.. XXX Document the excinfoattrs?
+
+.. exception:: NotShareableError
+
+   This exception, a subclass of :exc:`TypeError`, is raised when
+   an object cannot be sent to another interpreter.
+
+
+.. XXX Add functions for communicating between interpreters.
+
+
+Basic usage
+-----------
+
+Creating an interpreter and running code in it::
+
+    from concurrent import interpreters
+
+    interp = interpreters.create()
+
+    # Run in the current OS thread.
+
+    interp.exec('print("spam!")')
+
+    interp.exec("""if True:
+        print('spam!')
+        """)
+
+    from textwrap import dedent
+    interp.exec(dedent("""
+        print('spam!')
+        """))
+
+    def run():
+        print('spam!')
+
+    interp.call(run)
+
+    # Run in new OS thread.
+
+    t = interp.call_in_thread(run)
+    t.join()
+
+
+.. XXX Explain about object "sharing".
diff --git a/Doc/library/concurrent.rst b/Doc/library/concurrent.rst
index 8caea78bbb57e8..748c72c733bba2 100644
--- a/Doc/library/concurrent.rst
+++ b/Doc/library/concurrent.rst
@@ -1,6 +1,7 @@
 The :mod:`!concurrent` package
 ==============================
 
-Currently, there is only one module in this package:
+This package contains the following modules:
 
 * :mod:`concurrent.futures` -- Launching parallel tasks
+* :mod:`concurrent.interpreters` -- Multiple interpreters in the same process
diff --git a/Doc/library/python.rst b/Doc/library/python.rst
index c2c231af7c3033..c5c762e11b99e5 100644
--- a/Doc/library/python.rst
+++ b/Doc/library/python.rst
@@ -27,3 +27,8 @@ overview:
    inspect.rst
    annotationlib.rst
    site.rst
+
+.. seealso::
+
+   * See the :mod:`concurrent.interpreters` module, which similarly
+     exposes core runtime functionality.
diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst
index 2c7460fa159331..e01310937a2447 100644
--- a/Doc/whatsnew/3.14.rst
+++ b/Doc/whatsnew/3.14.rst
@@ -83,6 +83,7 @@ and improvements in user-friendliness and correctness.
 .. PEP-sized items next.
 
 * :ref:`PEP 649 and 749: deferred evaluation of annotations 
<whatsnew314-pep649>`
+* :ref:`PEP 734: Multiple Interpreters in the Stdlib <whatsnew314-pep734>`
 * :ref:`PEP 741: Python Configuration C API <whatsnew314-pep741>`
 * :ref:`PEP 750: Template strings <whatsnew314-pep750>`
 * :ref:`PEP 758: Allow except and except* expressions without parentheses 
<whatsnew314-pep758>`
@@ -123,6 +124,101 @@ of Python.  See :ref:`below <whatsnew314-refcount>` for 
details.
 New features
 ============
 
+.. _whatsnew314-pep734:
+
+PEP 734: Multiple Interpreters in the Stdlib
+--------------------------------------------
+
+The CPython runtime supports running multiple copies of Python in the
+same process simultaneously and has done so for over 20 years.
+Each of these separate copies is called an "interpreter".
+However, the feature had been available only through the C-API.
+
+That limitation is removed in the 3.14 release,
+with the new :mod:`concurrent.interpreters` module.
+
+There are at least two notable reasons why using multiple interpreters
+is worth considering:
+
+* they support a new (to Python), human-friendly concurrency model
+* true multi-core parallelism
+
+For some use cases, concurrency in software enables efficiency and
+can simplify software, at a high level.  At the same time, implementing
+and maintaining all but the simplest concurrency is often a struggle
+for the human brain.  That especially applies to plain threads
+(for example, :mod:`threading`), where all memory is shared between all 
threads.
+
+With multiple isolated interpreters, you can take advantage of a class
+of concurrency models, like CSP or the actor model, that have found
+success in other programming languages, like Smalltalk, Erlang,
+Haskell, and Go.  Think of multiple interpreters like threads
+but with opt-in sharing.
+
+Regarding multi-core parallelism: as of the 3.12 release, interpreters
+are now sufficiently isolated from one another to be used in parallel.
+(See :pep:`684`.)  This unlocks a variety of CPU-intensive use cases
+for Python that were limited by the :term:`GIL`.
+
+Using multiple interpreters is similar in many ways to
+:mod:`multiprocessing`, in that they both provide isolated logical
+"processes" that can run in parallel, with no sharing by default.
+However, when using multiple interpreters, an application will use
+fewer system resources and will operate more efficiently (since it
+stays within the same process).  Think of multiple interpreters as
+having the isolation of processes with the efficiency of threads.
+
+.. XXX Add an example or two.
+.. XXX Link to the not-yet-added HOWTO doc.
+
+While the feature has been around for decades, multiple interpreters
+have not been used widely, due to low awareness and the lack of a stdlib
+module.  Consequently, they currently have several notable limitations,
+which will improve significantly now that the feature is finally
+going mainstream.
+
+Current limitations:
+
+* starting each interpreter has not been optimized yet
+* each interpreter uses more memory than necessary
+  (we will be working next on extensive internal sharing between
+  interpreters)
+* there aren't many options *yet* for truly sharing objects or other
+  data between interpreters (other than :type:`memoryview`)
+* many extension modules on PyPI are not compatible with multiple
+  interpreters yet (stdlib extension modules *are* compatible)
+* the approach to writing applications that use multiple isolated
+  interpreters is mostly unfamiliar to Python users, for now
+
+The impact of these limitations will depend on future CPython
+improvements, how interpreters are used, and what the community solves
+through PyPI packages.  Depending on the use case, the limitations may
+not have much impact, so try it out!
+
+Furthermore, future CPython releases will reduce or eliminate overhead
+and provide utilities that are less appropriate on PyPI.  In the
+meantime, most of the limitations can also be addressed through
+extension modules, meaning PyPI packages can fill any gap for 3.14, and
+even back to 3.12 where interpreters were finally properly isolated and
+stopped sharing the :term:`GIL`.  Likewise, we expect to slowly see
+libraries on PyPI for high-level abstractions on top of interpreters.
+
+Regarding extension modules, work is in progress to update some PyPI
+projects, as well as tools like Cython, pybind11, nanobind, and PyO3.
+The steps for isolating an extension module are found at
+:ref:`isolating-extensions-howto`.  Isolating a module has a lot of
+overlap with what is required to support
+:ref:`free-threading <whatsnew314-free-threaded-cpython>`,
+so the ongoing work in the community in that area will help accelerate
+support for multiple interpreters.
+
+Also added in 3.14: :ref:`concurrent.futures.InterpreterPoolExecutor
+<whatsnew314-concurrent-futures-interp-pool>`.
+
+.. seealso::
+   :pep:`734`.
+
+
 .. _whatsnew314-pep750:
 
 PEP 750: Template strings
@@ -1109,6 +1205,8 @@ calendar
 concurrent.futures
 ------------------
 
+.. _whatsnew314-concurrent-futures-interp-pool:
+
 * Add :class:`~concurrent.futures.InterpreterPoolExecutor`,
   which exposes "subinterpreters" (multiple Python interpreters in the
   same process) to Python code.  This is separate from the proposed API
diff --git a/Lib/concurrent/futures/interpreter.py 
b/Lib/concurrent/futures/interpreter.py
index a2c4fbfd3fb831..f12b4ac33cda27 100644
--- a/Lib/concurrent/futures/interpreter.py
+++ b/Lib/concurrent/futures/interpreter.py
@@ -167,7 +167,7 @@ def run(self, task):
             except _interpqueues.QueueError:
                 continue
             except ModuleNotFoundError:
-                # interpreters.queues doesn't exist, which means
+                # interpreters._queues doesn't exist, which means
                 # QueueEmpty doesn't.  Act as though it does.
                 continue
             else:
diff --git a/Lib/test/support/interpreters/__init__.py 
b/Lib/concurrent/interpreters/__init__.py
similarity index 95%
rename from Lib/test/support/interpreters/__init__.py
rename to Lib/concurrent/interpreters/__init__.py
index 6d1b0690805d2d..0fd661249a276c 100644
--- a/Lib/test/support/interpreters/__init__.py
+++ b/Lib/concurrent/interpreters/__init__.py
@@ -9,6 +9,10 @@
     InterpreterError, InterpreterNotFoundError, NotShareableError,
     is_shareable,
 )
+from ._queues import (
+    create as create_queue,
+    Queue, QueueEmpty, QueueFull,
+)
 
 
 __all__ = [
@@ -20,21 +24,6 @@
 ]
 
 
-_queuemod = None
-
-def __getattr__(name):
-    if name in ('Queue', 'QueueEmpty', 'QueueFull', 'create_queue'):
-        global create_queue, Queue, QueueEmpty, QueueFull
-        ns = globals()
-        from .queues import (
-            create as create_queue,
-            Queue, QueueEmpty, QueueFull,
-        )
-        return ns[name]
-    else:
-        raise AttributeError(name)
-
-
 _EXEC_FAILURE_STR = """
 {superstr}
 
diff --git a/Lib/test/support/interpreters/_crossinterp.py 
b/Lib/concurrent/interpreters/_crossinterp.py
similarity index 98%
rename from Lib/test/support/interpreters/_crossinterp.py
rename to Lib/concurrent/interpreters/_crossinterp.py
index 544e197ba4c028..f47eb693ac861c 100644
--- a/Lib/test/support/interpreters/_crossinterp.py
+++ b/Lib/concurrent/interpreters/_crossinterp.py
@@ -61,7 +61,7 @@ def __new__(cls):
 
     def __repr__(self):
         return f'{self._MODULE}.{self._NAME}'
-#        return f'interpreters.queues.UNBOUND'
+#        return f'interpreters._queues.UNBOUND'
 
 
 UNBOUND = object.__new__(UnboundItem)
diff --git a/Lib/test/support/interpreters/queues.py 
b/Lib/concurrent/interpreters/_queues.py
similarity index 100%
rename from Lib/test/support/interpreters/queues.py
rename to Lib/concurrent/interpreters/_queues.py
diff --git a/Lib/test/support/interpreters/channels.py 
b/Lib/test/support/channels.py
similarity index 98%
rename from Lib/test/support/interpreters/channels.py
rename to Lib/test/support/channels.py
index 3b6e0f0effd969..dfa86ba24dc134 100644
--- a/Lib/test/support/interpreters/channels.py
+++ b/Lib/test/support/channels.py
@@ -2,14 +2,14 @@
 
 import time
 import _interpchannels as _channels
-from . import _crossinterp
+from concurrent.interpreters import _crossinterp
 
 # aliases:
 from _interpchannels import (
     ChannelError, ChannelNotFoundError, ChannelClosedError,
     ChannelEmptyError, ChannelNotEmptyError,
 )
-from ._crossinterp import (
+from concurrent.interpreters._crossinterp import (
     UNBOUND_ERROR, UNBOUND_REMOVE,
 )
 
diff --git a/Lib/test/test__interpchannels.py b/Lib/test/test__interpchannels.py
index 88eee03a3de93a..858d31a73cf4f4 100644
--- a/Lib/test/test__interpchannels.py
+++ b/Lib/test/test__interpchannels.py
@@ -9,7 +9,7 @@
 from test.support import import_helper, skip_if_sanitizer
 
 _channels = import_helper.import_module('_interpchannels')
-from test.support.interpreters import _crossinterp
+from concurrent.interpreters import _crossinterp
 from test.test__interpreters import (
     _interpreters,
     _run_output,
diff --git a/Lib/test/test_concurrent_futures/test_interpreter_pool.py 
b/Lib/test/test_concurrent_futures/test_interpreter_pool.py
index f6c62ae4b2021b..5fd5684e1035e9 100644
--- a/Lib/test/test_concurrent_futures/test_interpreter_pool.py
+++ b/Lib/test/test_concurrent_futures/test_interpreter_pool.py
@@ -8,10 +8,10 @@
 from concurrent.futures.interpreter import (
     ExecutionFailed, BrokenInterpreterPool,
 )
+from concurrent.interpreters import _queues as queues
 import _interpreters
 from test import support
 import test.test_asyncio.utils as testasyncio_utils
-from test.support.interpreters import queues
 
 from .executor import ExecutorTest, mul
 from .util import BaseTestCase, InterpreterPoolMixin, setup_module
diff --git a/Lib/test/test_interpreters/test_api.py 
b/Lib/test/test_interpreters/test_api.py
index b3c9ef8efba37a..1403cd145b6787 100644
--- a/Lib/test/test_interpreters/test_api.py
+++ b/Lib/test/test_interpreters/test_api.py
@@ -13,11 +13,11 @@
 from test.support import import_helper
 # Raise SkipTest if subinterpreters not supported.
 _interpreters = import_helper.import_module('_interpreters')
+from concurrent import interpreters
 from test.support import Py_GIL_DISABLED
-from test.support import interpreters
 from test.support import force_not_colorized
 import test._crossinterp_definitions as defs
-from test.support.interpreters import (
+from concurrent.interpreters import (
     InterpreterError, InterpreterNotFoundError, ExecutionFailed,
 )
 from .utils import (
@@ -133,7 +133,7 @@ def test_in_subinterpreter(self):
         main, = interpreters.list_all()
         interp = interpreters.create()
         out = _run_output(interp, dedent("""
-            from test.support import interpreters
+            from concurrent import interpreters
             interp = interpreters.create()
             print(interp.id)
             """))
@@ -196,7 +196,7 @@ def test_subinterpreter(self):
         main = interpreters.get_main()
         interp = interpreters.create()
         out = _run_output(interp, dedent("""
-            from test.support import interpreters
+            from concurrent import interpreters
             cur = interpreters.get_current()
             print(cur.id)
             """))
@@ -213,7 +213,7 @@ def test_idempotent(self):
         with self.subTest('subinterpreter'):
             interp = interpreters.create()
             out = _run_output(interp, dedent("""
-                from test.support import interpreters
+                from concurrent import interpreters
                 cur = interpreters.get_current()
                 print(id(cur))
                 cur = interpreters.get_current()
@@ -225,7 +225,7 @@ def test_idempotent(self):
         with self.subTest('per-interpreter'):
             interp = interpreters.create()
             out = _run_output(interp, dedent("""
-                from test.support import interpreters
+                from concurrent import interpreters
                 cur = interpreters.get_current()
                 print(id(cur))
                 """))
@@ -582,7 +582,7 @@ def test_from_current(self):
         main, = interpreters.list_all()
         interp = interpreters.create()
         out = _run_output(interp, dedent(f"""
-            from test.support import interpreters
+            from concurrent import interpreters
             interp = interpreters.Interpreter({interp.id})
             try:
                 interp.close()
@@ -599,7 +599,7 @@ def test_from_sibling(self):
         self.assertEqual(set(interpreters.list_all()),
                          {main, interp1, interp2})
         interp1.exec(dedent(f"""
-            from test.support import interpreters
+            from concurrent import interpreters
             interp2 = interpreters.Interpreter({interp2.id})
             interp2.close()
             interp3 = interpreters.create()
@@ -806,7 +806,7 @@ def eggs():
                 ham()
             """)
         scriptfile = self.make_script('script.py', tempdir, text="""
-            from test.support import interpreters
+            from concurrent import interpreters
 
             def script():
                 import spam
@@ -827,7 +827,7 @@ def script():
                 ~~~~~~~~~~~^^^^^^^^
               {interpmod_line.strip()}
                 raise ExecutionFailed(excinfo)
-            test.support.interpreters.ExecutionFailed: RuntimeError: uh-oh!
+            concurrent.interpreters.ExecutionFailed: RuntimeError: uh-oh!
 
             Uncaught in the interpreter:
 
@@ -1281,7 +1281,7 @@ def run(text):
             # no module indirection
             with self.subTest('no indirection'):
                 text = run(f"""
-                    from test.support import interpreters
+                    from concurrent import interpreters
 
                     def spam():
                         # This a global var...
@@ -1301,7 +1301,7 @@ def run(interp, func):
                 """)
             with self.subTest('indirect as func, direct interp'):
                 text = run(f"""
-                    from test.support import interpreters
+                    from concurrent import interpreters
                     import mymod
 
                     def spam():
@@ -1317,7 +1317,7 @@ def spam():
 
             # indirect as func, indirect interp
             new_mod('mymod', f"""
-                from test.support import interpreters
+                from concurrent import interpreters
                 def run(func):
                     interp = interpreters.create()
                     return interp.call(func)
diff --git a/Lib/test/test_interpreters/test_channels.py 
b/Lib/test/test_interpreters/test_channels.py
index 0c027b17cea68c..109ddf344539ad 100644
--- a/Lib/test/test_interpreters/test_channels.py
+++ b/Lib/test/test_interpreters/test_channels.py
@@ -8,8 +8,8 @@
 from test.support import import_helper
 # Raise SkipTest if subinterpreters not supported.
 _channels = import_helper.import_module('_interpchannels')
-from test.support import interpreters
-from test.support.interpreters import channels
+from concurrent import interpreters
+from test.support import channels
 from .utils import _run_output, TestBase
 
 
@@ -171,7 +171,7 @@ def test_send_recv_main(self):
     def test_send_recv_same_interpreter(self):
         interp = interpreters.create()
         interp.exec(dedent("""
-            from test.support.interpreters import channels
+            from test.support import channels
             r, s = channels.create()
             orig = b'spam'
             s.send_nowait(orig)
@@ -244,7 +244,7 @@ def test_send_recv_nowait_main_with_default(self):
     def test_send_recv_nowait_same_interpreter(self):
         interp = interpreters.create()
         interp.exec(dedent("""
-            from test.support.interpreters import channels
+            from test.support import channels
             r, s = channels.create()
             orig = b'spam'
             s.send_nowait(orig)
@@ -387,7 +387,7 @@ def common(rch, sch, unbound=None, presize=0):
             interp = interpreters.create()
 
             _run_output(interp, dedent(f"""
-                from test.support.interpreters import channels
+                from test.support import channels
                 sch = channels.SendChannel({sch.id})
                 obj1 = b'spam'
                 obj2 = b'eggs'
@@ -482,7 +482,7 @@ def test_send_cleared_with_subinterpreter_mixed(self):
         self.assertEqual(_channels.get_count(rch.id), 0)
 
         _run_output(interp, dedent(f"""
-            from test.support.interpreters import channels
+            from test.support import channels
             sch = channels.SendChannel({sch.id})
             sch.send_nowait(1, unbounditems=channels.UNBOUND)
             sch.send_nowait(2, unbounditems=channels.UNBOUND_ERROR)
@@ -518,7 +518,7 @@ def test_send_cleared_with_subinterpreter_multiple(self):
 
         sch.send_nowait(1)
         _run_output(interp1, dedent(f"""
-            from test.support.interpreters import channels
+            from test.support import channels
             rch = channels.RecvChannel({rch.id})
             sch = channels.SendChannel({sch.id})
             obj1 = rch.recv()
@@ -526,7 +526,7 @@ def test_send_cleared_with_subinterpreter_multiple(self):
             sch.send_nowait(obj1, unbounditems=channels.UNBOUND_REMOVE)
             """))
         _run_output(interp2, dedent(f"""
-            from test.support.interpreters import channels
+            from test.support import channels
             rch = channels.RecvChannel({rch.id})
             sch = channels.SendChannel({sch.id})
             obj2 = rch.recv()
diff --git a/Lib/test/test_interpreters/test_lifecycle.py 
b/Lib/test/test_interpreters/test_lifecycle.py
index ac24f6568acd95..15537ac6cc8f82 100644
--- a/Lib/test/test_interpreters/test_lifecycle.py
+++ b/Lib/test/test_interpreters/test_lifecycle.py
@@ -119,7 +119,7 @@ def test_sys_path_0(self):
         # The main interpreter's sys.path[0] should be used by subinterpreters.
         script = '''
             import sys
-            from test.support import interpreters
+            from concurrent import interpreters
 
             orig = sys.path[0]
 
@@ -170,7 +170,7 @@ def test_gh_109793(self):
         # is reported, even when subinterpreters get cleaned up at the end.
         import subprocess
         argv = [sys.executable, '-c', '''if True:
-            from test.support import interpreters
+            from concurrent import interpreters
             interp = interpreters.create()
             raise Exception
             ''']
diff --git a/Lib/test/test_interpreters/test_queues.py 
b/Lib/test/test_interpreters/test_queues.py
index 64a2db1230d023..cbae66a14a9eaa 100644
--- a/Lib/test/test_interpreters/test_queues.py
+++ b/Lib/test/test_interpreters/test_queues.py
@@ -7,8 +7,8 @@
 from test.support import import_helper, Py_DEBUG
 # Raise SkipTest if subinterpreters not supported.
 _queues = import_helper.import_module('_interpqueues')
-from test.support import interpreters
-from test.support.interpreters import queues, _crossinterp
+from concurrent import interpreters
+from concurrent.interpreters import _queues as queues, _crossinterp
 import test._crossinterp_definitions as defs
 from .utils import _run_output, TestBase as _TestBase
 
@@ -127,7 +127,7 @@ def test_shareable(self):
 
         interp = interpreters.create()
         interp.exec(dedent(f"""
-            from test.support.interpreters import queues
+            from concurrent.interpreters import _queues as queues
             queue1 = queues.Queue({queue1.id})
             """));
 
@@ -325,7 +325,7 @@ def test_put_get_full_fallback(self):
     def test_put_get_same_interpreter(self):
         interp = interpreters.create()
         interp.exec(dedent("""
-            from test.support.interpreters import queues
+            from concurrent.interpreters import _queues as queues
             queue = queues.create()
             """))
         for methname in ('get', 'get_nowait'):
@@ -352,7 +352,7 @@ def test_put_get_different_interpreters(self):
                 out = _run_output(
                     interp,
                     dedent(f"""
-                        from test.support.interpreters import queues
+                        from concurrent.interpreters import _queues as queues
                         queue1 = queues.Queue({queue1.id})
                         queue2 = queues.Queue({queue2.id})
                         assert queue1.qsize() == 1, 'expected: queue1.qsize() 
== 1'
@@ -391,7 +391,7 @@ def common(queue, unbound=None, presize=0):
             interp = interpreters.create()
 
             _run_output(interp, dedent(f"""
-                from test.support.interpreters import queues
+                from concurrent.interpreters import _queues as queues
                 queue = queues.Queue({queue.id})
                 obj1 = b'spam'
                 obj2 = b'eggs'
@@ -469,7 +469,7 @@ def test_put_cleared_with_subinterpreter_mixed(self):
         queue = queues.create()
         interp = interpreters.create()
         _run_output(interp, dedent(f"""
-            from test.support.interpreters import queues
+            from concurrent.interpreters import _queues as queues
             queue = queues.Queue({queue.id})
             queue.put(1, unbounditems=queues.UNBOUND)
             queue.put(2, unbounditems=queues.UNBOUND_ERROR)
@@ -505,14 +505,14 @@ def test_put_cleared_with_subinterpreter_multiple(self):
 
         queue.put(1)
         _run_output(interp1, dedent(f"""
-            from test.support.interpreters import queues
+            from concurrent.interpreters import _queues as queues
             queue = queues.Queue({queue.id})
             obj1 = queue.get()
             queue.put(2, unbounditems=queues.UNBOUND)
             queue.put(obj1, unbounditems=queues.UNBOUND_REMOVE)
             """))
         _run_output(interp2, dedent(f"""
-            from test.support.interpreters import queues
+            from concurrent.interpreters import _queues as queues
             queue = queues.Queue({queue.id})
             obj2 = queue.get()
             obj1 = queue.get()
diff --git a/Lib/test/test_interpreters/test_stress.py 
b/Lib/test/test_interpreters/test_stress.py
index fae2f38cb5534b..e25e67a0d4f445 100644
--- a/Lib/test/test_interpreters/test_stress.py
+++ b/Lib/test/test_interpreters/test_stress.py
@@ -6,7 +6,7 @@
 from test.support import threading_helper
 # Raise SkipTest if subinterpreters not supported.
 import_helper.import_module('_interpreters')
-from test.support import interpreters
+from concurrent import interpreters
 from .utils import TestBase
 
 
diff --git a/Lib/test/test_interpreters/utils.py 
b/Lib/test/test_interpreters/utils.py
index fc4ad662e03b66..d5d307d4973c12 100644
--- a/Lib/test/test_interpreters/utils.py
+++ b/Lib/test/test_interpreters/utils.py
@@ -22,7 +22,7 @@
     import _interpreters
 except ImportError as exc:
     raise unittest.SkipTest(str(exc))
-from test.support import interpreters
+from concurrent import interpreters
 
 
 try:
diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py
index 10c670377427d4..94004b4d45cb78 100644
--- a/Lib/test/test_sys.py
+++ b/Lib/test/test_sys.py
@@ -24,7 +24,7 @@
 from test.support import force_not_colorized
 from test.support import SHORT_TIMEOUT
 try:
-    from test.support import interpreters
+    from concurrent import interpreters
 except ImportError:
     interpreters = None
 import textwrap
diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py
index a9a354647f56ca..c07b8133f0a2d1 100644
--- a/Lib/test/test_threading.py
+++ b/Lib/test/test_threading.py
@@ -28,7 +28,7 @@
 from test import support
 
 try:
-    from test.support import interpreters
+    from concurrent import interpreters
 except ImportError:
     interpreters = None
 
diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py
index 3097c7ddf05901..a117413301bebe 100644
--- a/Lib/test/test_types.py
+++ b/Lib/test/test_types.py
@@ -2513,15 +2513,16 @@ class SubinterpreterTests(unittest.TestCase):
     def setUpClass(cls):
         global interpreters
         try:
-            from test.support import interpreters
+            from concurrent import interpreters
         except ModuleNotFoundError:
             raise unittest.SkipTest('subinterpreters required')
-        import test.support.interpreters.channels
+        from test.support import channels  # noqa: F401
+        cls.create_channel = staticmethod(channels.create)
 
     @cpython_only
     @no_rerun('channels (and queues) might have a refleak; see gh-122199')
     def test_static_types_inherited_slots(self):
-        rch, sch = interpreters.channels.create()
+        rch, sch = self.create_channel()
 
         script = textwrap.dedent("""
             import test.support
@@ -2547,7 +2548,7 @@ def collate_results(raw):
         main_results = collate_results(raw)
 
         interp = interpreters.create()
-        interp.exec('from test.support import interpreters')
+        interp.exec('from concurrent import interpreters')
         interp.prepare_main(sch=sch)
         interp.exec(script)
         raw = rch.recv_nowait()
diff --git a/Makefile.pre.in b/Makefile.pre.in
index b5703fbe6ae974..66b34b779f27cb 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -2514,7 +2514,7 @@ XMLLIBSUBDIRS=  xml xml/dom xml/etree xml/parsers xml/sax
 LIBSUBDIRS=    asyncio \
                collections \
                compression compression/_common compression/zstd \
-               concurrent concurrent/futures \
+               concurrent concurrent/futures concurrent/interpreters \
                csv \
                ctypes ctypes/macholib \
                curses \
@@ -2573,7 +2573,6 @@ TESTSUBDIRS=      idlelib/idle_test \
                test/subprocessdata \
                test/support \
                test/support/_hypothesis_stubs \
-               test/support/interpreters \
                test/test_asyncio \
                test/test_capi \
                test/test_cext \
diff --git 
a/Misc/NEWS.d/next/Library/2025-05-30-09-46-21.gh-issue-134939.Pu3nnm.rst 
b/Misc/NEWS.d/next/Library/2025-05-30-09-46-21.gh-issue-134939.Pu3nnm.rst
new file mode 100644
index 00000000000000..2bda69bff52156
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-05-30-09-46-21.gh-issue-134939.Pu3nnm.rst
@@ -0,0 +1 @@
+Add the :mod:`concurrent.interpreters` module.  See :pep:`734`.
diff --git a/Modules/_interpchannelsmodule.c b/Modules/_interpchannelsmodule.c
index ea2e5f99dfa308..ee5e2b005e0a5b 100644
--- a/Modules/_interpchannelsmodule.c
+++ b/Modules/_interpchannelsmodule.c
@@ -220,6 +220,22 @@ wait_for_lock(PyThread_type_lock mutex, PY_TIMEOUT_T 
timeout)
     return 0;
 }
 
+static int
+ensure_highlevel_module_loaded(void)
+{
+    PyObject *highlevel =
+            PyImport_ImportModule("concurrent.interpreters._channels");
+    if (highlevel == NULL) {
+        PyErr_Clear();
+        highlevel = PyImport_ImportModule("test.support.channels");
+        if (highlevel == NULL) {
+            return -1;
+        }
+    }
+    Py_DECREF(highlevel);
+    return 0;
+}
+
 
 /* module state *************************************************************/
 
@@ -2742,15 +2758,9 @@ _get_current_channelend_type(int end)
     }
     if (cls == NULL) {
         // Force the module to be loaded, to register the type.
-        PyObject *highlevel = PyImport_ImportModule("interpreters.channels");
-        if (highlevel == NULL) {
-            PyErr_Clear();
-            highlevel = 
PyImport_ImportModule("test.support.interpreters.channels");
-            if (highlevel == NULL) {
-                return NULL;
-            }
+        if (ensure_highlevel_module_loaded() < 0) {
+            return NULL;
         }
-        Py_DECREF(highlevel);
         if (end == CHANNEL_SEND) {
             cls = state->send_channel_type;
         }
diff --git a/Modules/_interpqueuesmodule.c b/Modules/_interpqueuesmodule.c
index 71d8fd8716cd94..e22709d5119b7c 100644
--- a/Modules/_interpqueuesmodule.c
+++ b/Modules/_interpqueuesmodule.c
@@ -136,13 +136,10 @@ idarg_int64_converter(PyObject *arg, void *ptr)
 static int
 ensure_highlevel_module_loaded(void)
 {
-    PyObject *highlevel = PyImport_ImportModule("interpreters.queues");
+    PyObject *highlevel =
+            PyImport_ImportModule("concurrent.interpreters._queues");
     if (highlevel == NULL) {
-        PyErr_Clear();
-        highlevel = PyImport_ImportModule("test.support.interpreters.queues");
-        if (highlevel == NULL) {
-            return -1;
-        }
+        return -1;
     }
     Py_DECREF(highlevel);
     return 0;
@@ -299,7 +296,7 @@ add_QueueError(PyObject *mod)
 {
     module_state *state = get_module_state(mod);
 
-#define PREFIX "test.support.interpreters."
+#define PREFIX "concurrent.interpreters."
 #define ADD_EXCTYPE(NAME, BASE, DOC)                                    \
     assert(state->NAME == NULL);                                        \
     if (add_exctype(mod, &state->NAME, PREFIX #NAME, DOC, BASE) < 0) {  \
diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c
index 845c218e679ad2..804cb4e4d1c8ee 100644
--- a/Modules/_testinternalcapi.c
+++ b/Modules/_testinternalcapi.c
@@ -1788,9 +1788,9 @@ exec_interpreter(PyObject *self, PyObject *args, PyObject 
*kwargs)
 
 /* To run some code in a sub-interpreter.
 
-Generally you can use test.support.interpreters,
+Generally you can use the interpreters module,
 but we keep this helper as a distinct implementation.
-That's especially important for testing test.support.interpreters.
+That's especially important for testing the interpreters module.
 */
 static PyObject *
 run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs)
diff --git a/Python/crossinterp_exceptions.h b/Python/crossinterp_exceptions.h
index ca4ca1cf123e49..12cd61db1b6762 100644
--- a/Python/crossinterp_exceptions.h
+++ b/Python/crossinterp_exceptions.h
@@ -24,7 +24,7 @@ _ensure_current_cause(PyThreadState *tstate, PyObject *cause)
 
 static PyTypeObject _PyExc_InterpreterError = {
     PyVarObject_HEAD_INIT(NULL, 0)
-    .tp_name = "interpreters.InterpreterError",
+    .tp_name = "concurrent.interpreters.InterpreterError",
     .tp_doc = PyDoc_STR("A cross-interpreter operation failed"),
     .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
     //.tp_traverse = ((PyTypeObject *)PyExc_Exception)->tp_traverse,
@@ -37,7 +37,7 @@ PyObject *PyExc_InterpreterError = (PyObject 
*)&_PyExc_InterpreterError;
 
 static PyTypeObject _PyExc_InterpreterNotFoundError = {
     PyVarObject_HEAD_INIT(NULL, 0)
-    .tp_name = "interpreters.InterpreterNotFoundError",
+    .tp_name = "concurrent.interpreters.InterpreterNotFoundError",
     .tp_doc = PyDoc_STR("An interpreter was not found"),
     .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
     //.tp_traverse = ((PyTypeObject *)PyExc_Exception)->tp_traverse,
@@ -51,7 +51,7 @@ PyObject *PyExc_InterpreterNotFoundError = (PyObject 
*)&_PyExc_InterpreterNotFou
 static int
 _init_notshareableerror(exceptions_t *state)
 {
-    const char *name = "interpreters.NotShareableError";
+    const char *name = "concurrent.interpreters.NotShareableError";
     PyObject *base = PyExc_TypeError;
     PyObject *ns = NULL;
     PyObject *exctype = PyErr_NewException(name, base, ns);

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: arch...@mail-archive.com

Reply via email to