]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-134939: Add the concurrent.interpreters Module (gh-133958)
authorEric Snow <ericsnowcurrently@gmail.com>
Wed, 11 Jun 2025 23:35:48 +0000 (17:35 -0600)
committerGitHub <noreply@github.com>
Wed, 11 Jun 2025 23:35:48 +0000 (17:35 -0600)
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.)

28 files changed:
.github/CODEOWNERS
Doc/library/concurrency.rst
Doc/library/concurrent.interpreters.rst [new file with mode: 0644]
Doc/library/concurrent.rst
Doc/library/python.rst
Doc/whatsnew/3.14.rst
Lib/concurrent/futures/interpreter.py
Lib/concurrent/interpreters/__init__.py [moved from Lib/test/support/interpreters/__init__.py with 95% similarity]
Lib/concurrent/interpreters/_crossinterp.py [moved from Lib/test/support/interpreters/_crossinterp.py with 98% similarity]
Lib/concurrent/interpreters/_queues.py [moved from Lib/test/support/interpreters/queues.py with 100% similarity]
Lib/test/support/channels.py [moved from Lib/test/support/interpreters/channels.py with 98% similarity]
Lib/test/test__interpchannels.py
Lib/test/test_concurrent_futures/test_interpreter_pool.py
Lib/test/test_interpreters/test_api.py
Lib/test/test_interpreters/test_channels.py
Lib/test/test_interpreters/test_lifecycle.py
Lib/test/test_interpreters/test_queues.py
Lib/test/test_interpreters/test_stress.py
Lib/test/test_interpreters/utils.py
Lib/test/test_sys.py
Lib/test/test_threading.py
Lib/test/test_types.py
Makefile.pre.in
Misc/NEWS.d/next/Library/2025-05-30-09-46-21.gh-issue-134939.Pu3nnm.rst [new file with mode: 0644]
Modules/_interpchannelsmodule.c
Modules/_interpqueuesmodule.c
Modules/_testinternalcapi.c
Python/crossinterp_exceptions.h

index 775d9c63260c83e0faebcad83e1742418248e709..63a2849004389939ace1189484dd3a3e0f2e6301 100644 (file)
@@ -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
index 5be1a1106b09a0dfea2fba450e5e5437c0f413e0..18f9443cbfea20fff00a80c98918b8d844ff18de 100644 (file)
@@ -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 (file)
index 0000000..8860418
--- /dev/null
@@ -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 <ericsnowcurrently@gmail.com>
+.. sectionauthor:: Eric Snow <ericsnowcurrently@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".
index 8caea78bbb57e82a4d34d31bc11585fb0a36466b..748c72c733bba26de8eef4e8163403e0f43d151d 100644 (file)
@@ -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
index c2c231af7c3033031cc200aa83e588664792ff0f..c5c762e11b99e5111b23b0b0696e239f254413f5 100644 (file)
@@ -27,3 +27,8 @@ overview:
    inspect.rst
    annotationlib.rst
    site.rst
+
+.. seealso::
+
+   * See the :mod:`concurrent.interpreters` module, which similarly
+     exposes core runtime functionality.
index 45e68aea5fb9a2618af9d826d1dd349a2f15198a..ca330a32b33c4b005097f17d7834bc0444402868 100644 (file)
@@ -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
index a2c4fbfd3fb831caa791ea45364e91517938d9fd..f12b4ac33cda27b130d8722875f2ef6f95a93b0f 100644 (file)
@@ -167,7 +167,7 @@ class WorkerContext(_thread.WorkerContext):
             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:
similarity index 95%
rename from Lib/test/support/interpreters/__init__.py
rename to Lib/concurrent/interpreters/__init__.py
index 6d1b0690805d2d699c25f37466f3961d8e98547d..0fd661249a276cb435624ecc86a37496f55534d3 100644 (file)
@@ -9,6 +9,10 @@ from _interpreters import (
     InterpreterError, InterpreterNotFoundError, NotShareableError,
     is_shareable,
 )
+from ._queues import (
+    create as create_queue,
+    Queue, QueueEmpty, QueueFull,
+)
 
 
 __all__ = [
@@ -20,21 +24,6 @@ __all__ = [
 ]
 
 
-_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}
 
similarity index 98%
rename from Lib/test/support/interpreters/_crossinterp.py
rename to Lib/concurrent/interpreters/_crossinterp.py
index 544e197ba4c02880cc83e46f52e75d9b5ab6e40c..f47eb693ac861c0721a04e6de130b06f2e581ab7 100644 (file)
@@ -61,7 +61,7 @@ class UnboundItem:
 
     def __repr__(self):
         return f'{self._MODULE}.{self._NAME}'
-#        return f'interpreters.queues.UNBOUND'
+#        return f'interpreters._queues.UNBOUND'
 
 
 UNBOUND = object.__new__(UnboundItem)
similarity index 98%
rename from Lib/test/support/interpreters/channels.py
rename to Lib/test/support/channels.py
index 1724759b75a999b8ea84006bb2ca3156b7390f63..b2de24d9d3e53467c5e97c0334a788942320c068 100644 (file)
@@ -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,  # noqa: F401
     ChannelEmptyError, ChannelNotEmptyError,  # noqa: F401
 )
-from ._crossinterp import (
+from concurrent.interpreters._crossinterp import (
     UNBOUND_ERROR, UNBOUND_REMOVE,
 )
 
index 88eee03a3de93ad01912262263c4fd2dd09e540e..858d31a73cf4f458b7faa4a67c248c0436fe6ceb 100644 (file)
@@ -9,7 +9,7 @@ import unittest
 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,
index f6c62ae4b2021b28cb97148a7a53c7aa7b04e664..5fd5684e1035e9194fa16cf348e6aeb7d248b379 100644 (file)
@@ -8,10 +8,10 @@ import unittest
 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
index b3c9ef8efba37ad1c19cd0bddf5a09dc88b4a5b6..1403cd145b6787ab5e5458e547f6a76ad3c6f641 100644 (file)
@@ -13,11 +13,11 @@ from test.support import script_helper
 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 @@ class CreateTests(TestBase):
         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 @@ class GetCurrentTests(TestBase):
         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 @@ class GetCurrentTests(TestBase):
         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 @@ class GetCurrentTests(TestBase):
         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 @@ class TestInterpreterClose(TestBase):
         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 @@ class TestInterpreterClose(TestBase):
         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 @@ class TestInterpreterExec(TestBase):
                 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 @@ class TestInterpreterExec(TestBase):
                 ~~~~~~~~~~~^^^^^^^^
               {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 @@ class TestInterpreterCall(TestBase):
             # 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 @@ class TestInterpreterCall(TestBase):
                 """)
             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 @@ class TestInterpreterCall(TestBase):
 
             # 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)
index 0c027b17cea68c537dd4426ac75500202953b976..109ddf344539adaa6f09ac99cce05ce1366837c7 100644 (file)
@@ -8,8 +8,8 @@ import time
 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 @@ class TestSendRecv(TestBase):
     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 @@ class TestSendRecv(TestBase):
     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 @@ class TestSendRecv(TestBase):
             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 @@ class TestSendRecv(TestBase):
         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 @@ class TestSendRecv(TestBase):
 
         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 @@ class TestSendRecv(TestBase):
             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()
index ac24f6568acd95224c906dc8e78bc1fb04f43ef9..15537ac6cc8f823b0549f29a54d9883380ccc45e 100644 (file)
@@ -119,7 +119,7 @@ class StartupTests(TestBase):
         # 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 @@ class FinalizationTests(TestBase):
         # 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
             ''']
index 757373904d7a431fbc338f7103db2c4132700898..3e982d76e863141d6b0452b7a841c934392d98fb 100644 (file)
@@ -7,8 +7,8 @@ import unittest
 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
 from .utils import _run_output, TestBase as _TestBase
 
 
@@ -126,7 +126,7 @@ class QueueTests(TestBase):
 
         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})
             """));
 
@@ -324,7 +324,7 @@ class TestQueueOps(TestBase):
     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'):
@@ -351,7 +351,7 @@ class TestQueueOps(TestBase):
                 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'
@@ -390,7 +390,7 @@ class TestQueueOps(TestBase):
             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'
@@ -468,7 +468,7 @@ class TestQueueOps(TestBase):
         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)
@@ -504,14 +504,14 @@ class TestQueueOps(TestBase):
 
         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()
index fae2f38cb5534bc62509a94963d3f3a33b17e5d9..e25e67a0d4f4453ee01f8c1c4a0b1e081173be69 100644 (file)
@@ -6,7 +6,7 @@ from test.support import import_helper
 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
 
 
index c25e0fb7475e7ed036c897a1c9d778eeabc48768..ae09aa457b48c7a9a55fcf24de19deca63e067f3 100644 (file)
@@ -21,7 +21,7 @@ try:
     import _interpreters
 except ImportError as exc:
     raise unittest.SkipTest(str(exc))
-from test.support import interpreters
+from concurrent import interpreters
 
 
 try:
index bf415894903e9ba15928a103eda81bcde74d75b3..39e62027f03e5a6f5ca7dd18c7baad1ec5a73d15 100644 (file)
@@ -24,7 +24,7 @@ from test.support import import_helper
 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
index 59b3a749d2fffaf2d901822e76955c5a06adfa73..125c27446986c0abbf5274ce2e58c9f5a79f3f47 100644 (file)
@@ -28,7 +28,7 @@ from test import lock_tests
 from test import support
 
 try:
-    from test.support import interpreters
+    from concurrent import interpreters
 except ImportError:
     interpreters = None
 
index 9011e0e196282064f0e21456e41c654158f49c52..a117413301bebe99a0e9763600c6e16a92873a05 100644 (file)
@@ -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  # noqa: F401
+        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 @@ class SubinterpreterTests(unittest.TestCase):
         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()
index b5703fbe6ae9748ebf778a8a8e98a726e0ab7b99..66b34b779f27cb93a3c89fb0083ee24712744d3f 100644 (file)
@@ -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 (file)
index 0000000..2bda69b
--- /dev/null
@@ -0,0 +1 @@
+Add the :mod:`concurrent.interpreters` module.  See :pep:`734`.
index ea2e5f99dfa30834869f3d29819bb15d850aedfc..ee5e2b005e0a5babd91f777384f0a322215c89ba 100644 (file)
@@ -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;
         }
index 71d8fd8716cd9414affc28f5df10a5b2ee3cdd51..e22709d5119b7c655af49ee6e1937efbca156b33 100644 (file)
@@ -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) {  \
index 845c218e679ad249da32a71379c57bc230630054..804cb4e4d1c8eecf9a4efbf2d3dc70c89624966b 100644 (file)
@@ -1788,9 +1788,9 @@ finally:
 
 /* 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)
index ca4ca1cf123e494d4368b4219f575a5afd70ee0c..12cd61db1b676242912692f118f66ee4478e567b 100644 (file)
@@ -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);