diff --git a/Doc/library/code.rst b/Doc/library/code.rst
index 59c016d21501b0..1e7127ee957b63 100644
--- a/Doc/library/code.rst
+++ b/Doc/library/code.rst
@@ -40,7 +40,7 @@ build applications which provide an interactive interpreter prompt.
.. versionchanged:: 3.13
Added *local_exit* parameter.
-.. function:: interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=False)
+.. function:: interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=False, *, use_pyrepl=False)
Convenience function to run a read-eval-print loop. This creates a new
instance of :class:`InteractiveConsole` and sets *readfunc* to be used as
@@ -49,8 +49,9 @@ build applications which provide an interactive interpreter prompt.
use as the default namespace for the interpreter loop. If *local_exit* is provided,
it is passed to the :class:`InteractiveConsole` constructor. The :meth:`~InteractiveConsole.interact`
method of the instance is then run with *banner* and *exitmsg* passed as the
- banner and exit message to use, if provided. The console object is discarded
- after use.
+ banner and exit message to use, if provided. The *use_pyrepl* argument is
+ passed to :meth:`~InteractiveConsole.interact` to control whether to use the
+ pyrepl-based REPL. The console object is discarded after use.
.. versionchanged:: 3.6
Added *exitmsg* parameter.
@@ -58,6 +59,9 @@ build applications which provide an interactive interpreter prompt.
.. versionchanged:: 3.13
Added *local_exit* parameter.
+ .. versionchanged:: next
+ Added *use_pyrepl* parameter.
+
.. function:: compile_command(source, filename="", symbol="single")
This function is useful for programs that want to emulate Python's interpreter
@@ -153,7 +157,7 @@ The :class:`InteractiveConsole` class is a subclass of
interpreter objects as well as the following additions.
-.. method:: InteractiveConsole.interact(banner=None, exitmsg=None)
+.. method:: InteractiveConsole.interact(banner=None, exitmsg=None, *, use_pyrepl=False)
Closely emulate the interactive Python console. The optional *banner* argument
specify the banner to print before the first interaction; by default it prints a
@@ -165,12 +169,21 @@ interpreter objects as well as the following additions.
Pass the empty string to suppress the exit message. If *exitmsg* is not given or
``None``, a default message is printed.
+ The optional *use_pyrepl* argument controls whether to use the pyrepl-based REPL
+ when available. When ``True``, pyrepl is used. When ``False`` (the default),
+ the basic readline-based REPL is used. When ``None``, pyrepl is used
+ automatically if available and the :envvar:`PYTHON_BASIC_REPL` environment
+ variable is not set.
+
.. versionchanged:: 3.4
To suppress printing any banner, pass an empty string.
.. versionchanged:: 3.6
Print an exit message when exiting.
+ .. versionchanged:: next
+ Added *use_pyrepl* parameter.
+
.. method:: InteractiveConsole.push(line)
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 6543da4487e42f..757641f8106436 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -722,6 +722,18 @@ calendar
(Contributed by Pål Grønås Drange in :gh:`140212`.)
+code
+----
+
+* The :meth:`code.InteractiveConsole.interact` method and the :func:`code.interact`
+ function now support a *use_pyrepl* parameter to control whether to use the
+ pyrepl-based REPL. By default, *use_pyrepl* is ``False``, using the basic
+ readline-based REPL. When set to ``True``, pyrepl is used if available.
+ When set to ``None``, pyrepl is used automatically if available and the
+ :envvar:`PYTHON_BASIC_REPL` environment variable is not set.
+ (Contributed by Long Tan in :gh:`148261`.)
+
+
collections
-----------
@@ -830,9 +842,9 @@ http.server
is colored by default.
This can be controlled with :ref:`environment variables
`.
- (Contributed by Hugo van Kemenade in :gh:`146292`.)
-
-
+ (Contributed by Hugo van Kemenade in :gh:`146292`.)
+
+
inspect
-------
diff --git a/Lib/code.py b/Lib/code.py
index f7e275d8801b7c..e842f212b50c1c 100644
--- a/Lib/code.py
+++ b/Lib/code.py
@@ -6,6 +6,7 @@
import builtins
+import os
import sys
import traceback
from codeop import CommandCompiler, compile_command
@@ -201,7 +202,7 @@ def resetbuffer(self):
"""Reset the input buffer."""
self.buffer = []
- def interact(self, banner=None, exitmsg=None):
+ def interact(self, banner=None, exitmsg=None, *, use_pyrepl=False):
"""Closely emulate the interactive Python console.
The optional banner argument specifies the banner to print
@@ -216,7 +217,29 @@ def interact(self, banner=None, exitmsg=None):
printing an exit message. If exitmsg is not given or None,
a default message is printed.
+ The use_pyrepl argument controls whether to use the pyrepl-based REPL
+ when available. When True, pyrepl is used. When False (the default),
+ the basic readline-based REPL is used. When None, pyrepl is used
+ automatically if available and the PYTHON_BASIC_REPL environment
+ variable is not set.
+
"""
+ if use_pyrepl is None:
+ use_pyrepl = not os.getenv('PYTHON_BASIC_REPL')
+
+ if use_pyrepl:
+ try:
+ from _pyrepl.main import CAN_USE_PYREPL
+ if CAN_USE_PYREPL:
+ from _pyrepl.simple_interact import (
+ run_multiline_interactive_console,
+ )
+ run_multiline_interactive_console(self)
+ return
+ except ImportError:
+ pass
+ # Fall through to basic REPL if pyrepl is unavailable
+
try:
sys.ps1
delete_ps1_after = False
@@ -355,7 +378,7 @@ def __call__(self, code=None):
raise SystemExit(code)
-def interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=False):
+def interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=False, *, use_pyrepl=False):
"""Closely emulate the interactive Python interpreter.
This is a backwards compatible interface to the InteractiveConsole
@@ -369,6 +392,7 @@ def interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=Fa
local -- passed to InteractiveInterpreter.__init__()
exitmsg -- passed to InteractiveConsole.interact()
local_exit -- passed to InteractiveConsole.__init__()
+ use_pyrepl -- passed to InteractiveConsole.interact()
"""
console = InteractiveConsole(local, local_exit=local_exit)
@@ -379,7 +403,7 @@ def interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=Fa
import readline # noqa: F401
except ImportError:
pass
- console.interact(banner, exitmsg)
+ console.interact(banner, exitmsg, use_pyrepl=use_pyrepl)
if __name__ == "__main__":
diff --git a/Lib/test/test_code_module.py b/Lib/test/test_code_module.py
index 3642b47c2c1f03..1d8fe59ff4d028 100644
--- a/Lib/test/test_code_module.py
+++ b/Lib/test/test_code_module.py
@@ -1,4 +1,5 @@
"Test InteractiveConsole and InteractiveInterpreter from code module"
+import os
import sys
import traceback
import unittest
@@ -44,7 +45,7 @@ def test_ps1(self):
"code.sys.ps1",
EOFError('Finished')
]
- self.console.interact()
+ self.console.interact(use_pyrepl=False)
output = ''.join(''.join(call[1]) for call in self.stdout.method_calls)
self.assertIn('>>> ', output)
self.assertNotHasAttr(self.sysmod, 'ps1')
@@ -55,7 +56,7 @@ def test_ps1(self):
EOFError('Finished')
]
self.sysmod.ps1 = 'custom1> '
- self.console.interact()
+ self.console.interact(use_pyrepl=False)
output = ''.join(''.join(call[1]) for call in self.stdout.method_calls)
self.assertIn('custom1> ', output)
self.assertEqual(self.sysmod.ps1, 'custom1> ')
@@ -66,7 +67,7 @@ def test_ps2(self):
"code.sys.ps2",
EOFError('Finished')
]
- self.console.interact()
+ self.console.interact(use_pyrepl=False)
output = ''.join(''.join(call[1]) for call in self.stdout.method_calls)
self.assertIn('... ', output)
self.assertNotHasAttr(self.sysmod, 'ps2')
@@ -77,14 +78,14 @@ def test_ps2(self):
EOFError('Finished')
]
self.sysmod.ps2 = 'custom2> '
- self.console.interact()
+ self.console.interact(use_pyrepl=False)
output = ''.join(''.join(call[1]) for call in self.stdout.method_calls)
self.assertIn('custom2> ', output)
self.assertEqual(self.sysmod.ps2, 'custom2> ')
def test_console_stderr(self):
self.infunc.side_effect = ["'antioch'", "", EOFError('Finished')]
- self.console.interact()
+ self.console.interact(use_pyrepl=False)
for call in list(self.stdout.method_calls):
if 'antioch' in ''.join(call[1]):
break
@@ -96,7 +97,7 @@ def test_syntax_error(self):
" x = ?",
"",
EOFError('Finished')]
- self.console.interact()
+ self.console.interact(use_pyrepl=False)
output = ''.join(''.join(call[1]) for call in self.stderr.method_calls)
output = output[output.index('(InteractiveConsole)'):]
output = output[:output.index('\nnow exiting')]
@@ -113,7 +114,7 @@ def test_syntax_error(self):
def test_indentation_error(self):
self.infunc.side_effect = [" 1", EOFError('Finished')]
- self.console.interact()
+ self.console.interact(use_pyrepl=False)
output = ''.join(''.join(call[1]) for call in self.stderr.method_calls)
output = output[output.index('(InteractiveConsole)'):]
output = output[:output.index('\nnow exiting')]
@@ -129,7 +130,7 @@ def test_indentation_error(self):
def test_unicode_error(self):
self.infunc.side_effect = ["'\ud800'", EOFError('Finished')]
- self.console.interact()
+ self.console.interact(use_pyrepl=False)
output = ''.join(''.join(call[1]) for call in self.stderr.method_calls)
output = output[output.index('(InteractiveConsole)'):]
output = output[output.index('\n') + 1:]
@@ -148,7 +149,7 @@ def test_sysexcepthook(self):
EOFError('Finished')]
hook = mock.Mock()
self.sysmod.excepthook = hook
- self.console.interact()
+ self.console.interact(use_pyrepl=False)
hook.assert_called()
hook.assert_called_with(self.sysmod.last_type,
self.sysmod.last_value,
@@ -170,7 +171,7 @@ def test_sysexcepthook_syntax_error(self):
EOFError('Finished')]
hook = mock.Mock()
self.sysmod.excepthook = hook
- self.console.interact()
+ self.console.interact(use_pyrepl=False)
hook.assert_called()
hook.assert_called_with(self.sysmod.last_type,
self.sysmod.last_value,
@@ -190,7 +191,7 @@ def test_sysexcepthook_indentation_error(self):
self.infunc.side_effect = [" 1", EOFError('Finished')]
hook = mock.Mock()
self.sysmod.excepthook = hook
- self.console.interact()
+ self.console.interact(use_pyrepl=False)
hook.assert_called()
hook.assert_called_with(self.sysmod.last_type,
self.sysmod.last_value,
@@ -208,7 +209,7 @@ def test_sysexcepthook_indentation_error(self):
def test_sysexcepthook_crashing_doesnt_close_repl(self):
self.infunc.side_effect = ["1/0", "a = 123", "print(a)", EOFError('Finished')]
self.sysmod.excepthook = 1
- self.console.interact()
+ self.console.interact(use_pyrepl=False)
self.assertEqual(['write', ('123', ), {}], self.stdout.method_calls[0])
error = "".join(call.args[0] for call in self.stderr.method_calls if call[0] == 'write')
self.assertIn("Error in sys.excepthook:", error)
@@ -222,7 +223,7 @@ def test_sysexcepthook_raising_BaseException(self):
def raise_base(*args, **kwargs):
raise BaseException(s)
self.sysmod.excepthook = raise_base
- self.console.interact()
+ self.console.interact(use_pyrepl=False)
self.assertEqual(['write', ('123', ), {}], self.stdout.method_calls[0])
error = "".join(call.args[0] for call in self.stderr.method_calls if call[0] == 'write')
self.assertIn("Error in sys.excepthook:", error)
@@ -236,12 +237,12 @@ def raise_base(*args, **kwargs):
raise SystemExit
self.sysmod.excepthook = raise_base
with self.assertRaises(SystemExit):
- self.console.interact()
+ self.console.interact(use_pyrepl=False)
def test_banner(self):
# with banner
self.infunc.side_effect = EOFError('Finished')
- self.console.interact(banner='Foo')
+ self.console.interact(banner='Foo', use_pyrepl=False)
self.assertEqual(len(self.stderr.method_calls), 3)
banner_call = self.stderr.method_calls[0]
self.assertEqual(banner_call, ['write', ('Foo\n',), {}])
@@ -249,13 +250,13 @@ def test_banner(self):
# no banner
self.stderr.reset_mock()
self.infunc.side_effect = EOFError('Finished')
- self.console.interact(banner='')
+ self.console.interact(banner='', use_pyrepl=False)
self.assertEqual(len(self.stderr.method_calls), 2)
def test_exit_msg(self):
# default exit message
self.infunc.side_effect = EOFError('Finished')
- self.console.interact(banner='')
+ self.console.interact(banner='', use_pyrepl=False)
self.assertEqual(len(self.stderr.method_calls), 2)
err_msg = self.stderr.method_calls[1]
expected = 'now exiting InteractiveConsole...\n'
@@ -264,7 +265,7 @@ def test_exit_msg(self):
# no exit message
self.stderr.reset_mock()
self.infunc.side_effect = EOFError('Finished')
- self.console.interact(banner='', exitmsg='')
+ self.console.interact(banner='', exitmsg='', use_pyrepl=False)
self.assertEqual(len(self.stderr.method_calls), 1)
# custom exit message
@@ -273,7 +274,7 @@ def test_exit_msg(self):
'bye! \N{GREEK SMALL LETTER ZETA}\N{CYRILLIC SMALL LETTER ZHE}'
)
self.infunc.side_effect = EOFError('Finished')
- self.console.interact(banner='', exitmsg=message)
+ self.console.interact(banner='', exitmsg=message, use_pyrepl=False)
self.assertEqual(len(self.stderr.method_calls), 2)
err_msg = self.stderr.method_calls[1]
expected = message + '\n'
@@ -283,7 +284,7 @@ def test_exit_msg(self):
def test_cause_tb(self):
self.infunc.side_effect = ["raise ValueError('') from AttributeError",
EOFError('Finished')]
- self.console.interact()
+ self.console.interact(use_pyrepl=False)
output = ''.join(''.join(call[1]) for call in self.stderr.method_calls)
expected = dedent("""
AttributeError
@@ -304,7 +305,7 @@ def test_cause_tb(self):
def test_context_tb(self):
self.infunc.side_effect = ["try: ham\nexcept: eggs\n",
EOFError('Finished')]
- self.console.interact()
+ self.console.interact(use_pyrepl=False)
output = ''.join(''.join(call[1]) for call in self.stderr.method_calls)
expected = dedent("""
Traceback (most recent call last):
@@ -335,12 +336,60 @@ def setUp(self):
def test_exit(self):
# default exit message
self.infunc.side_effect = ["exit()"]
- self.console.interact(banner='')
+ self.console.interact(banner='', use_pyrepl=False)
self.assertEqual(len(self.stderr.method_calls), 2)
err_msg = self.stderr.method_calls[1]
expected = 'now exiting InteractiveConsole...\n'
self.assertEqual(err_msg, ['write', (expected,), {}])
+class TestInteractiveConsoleUsePyrepl(unittest.TestCase, MockSys):
+ """Tests for the use_pyrepl parameter of InteractiveConsole.interact()."""
+
+ def setUp(self):
+ self.console = code.InteractiveConsole()
+ self.mock_sys()
+
+ def test_use_pyrepl_false_uses_basic_repl(self):
+ """When use_pyrepl=False, the basic REPL should be used."""
+ self.infunc.side_effect = ["'test'", EOFError('Finished')]
+ self.console.interact(banner='', use_pyrepl=False)
+ # Should have used code.input (the basic REPL)
+ self.infunc.assert_called()
+
+ @mock.patch.object(os, 'getenv', return_value='1')
+ def test_python_basic_repl_env_uses_basic_repl(self, mock_getenv):
+ """When PYTHON_BASIC_REPL is set, the basic REPL should be used."""
+ self.infunc.side_effect = ["'test'", EOFError('Finished')]
+ self.console.interact(banner='')
+ # Should have used code.input (the basic REPL)
+ self.infunc.assert_called()
+
+ def test_use_pyrepl_false_with_input(self):
+ """Test that use_pyrepl=False correctly processes input."""
+ self.infunc.side_effect = [
+ "x = 1",
+ "x",
+ EOFError('Finished')
+ ]
+ self.console.interact(banner='', use_pyrepl=False)
+ output = ''.join(''.join(call[1]) for call in self.stdout.method_calls)
+ self.assertIn('1', output)
+
+
+class TestInteractFunctionUsePyrepl(unittest.TestCase, MockSys):
+ """Tests for the use_pyrepl parameter of the top-level interact() function."""
+
+ def setUp(self):
+ self.mock_sys()
+
+ def test_interact_use_pyrepl_false(self):
+ """When use_pyrepl=False, the basic REPL should be used."""
+ self.infunc.side_effect = ["'test'", EOFError('Finished')]
+ with mock.patch('code.input', create=True, side_effect=self.infunc):
+ code.interact(banner='', use_pyrepl=False)
+ self.infunc.assert_called()
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/Misc/NEWS.d/next/Library/2026-04-09-00-38-52.gh-issue-119512.Z6V-kp.rst b/Misc/NEWS.d/next/Library/2026-04-09-00-38-52.gh-issue-119512.Z6V-kp.rst
new file mode 100644
index 00000000000000..d666f710a4328c
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-04-09-00-38-52.gh-issue-119512.Z6V-kp.rst
@@ -0,0 +1 @@
+Use new REPL for the :mod:`code` module.