Skip to content

Make custom REPLs and the -i flag work together #140018

@bswck

Description

@bswck

Bug report

Bug description:

Combining a custom REPL with -i is currently ramshackle, because pymain_run_python doesn't know that something (either a file, or a module, or stdin) acted as a REPL and therefore a new one right after executing the original code isn't desired:

cpython/Modules/main.c

Lines 681 to 697 in 5f91d5d

if (config->run_command) {
*exitcode = pymain_run_command(config->run_command);
}
else if (config->run_module) {
*exitcode = pymain_run_module(config->run_module, 1);
}
else if (main_importer_path != NULL) {
*exitcode = pymain_run_module(L"__main__", 0);
}
else if (config->run_filename != NULL) {
*exitcode = pymain_run_file(config);
}
else {
*exitcode = pymain_run_stdin(config);
}
pymain_repl(config, exitcode);

This leads to broken expectations around python -i -m pdb or python -i -m asyncio:

Broken output of python -i -m asyncio

❯ ./python -i -m asyncio
asyncio REPL 3.15.0a0 (heads/main:5f91d5d9a41, Oct 12 2025, 22:21:59) [GCC 15.2.1 20250808 (Red Hat 15.2.1-1)] on linux
Use "await" directly instead of "asyncio.run()".
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> exit
exiting asyncio REPL...
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/home/bswck/Python/cpython/Lib/asyncio/__main__.py", line 240, in <module>
    sys.exit(return_code)
    ~~~~~~~~^^^^^^^^^^^^^
SystemExit: 0
>>> exit

Broken output of python -i -m pdb

❯ ./python -i -m pdb
usage: python -m pdb [-h] [-c command] (-m module | -p pid | pyfile) [args ...]

Debug the Python program given by pyfile. Alternatively,
an executable module or package to debug can be specified using
the -m switch. You can also attach to a running Python process
using the -p option with its PID.

Initial commands are read from .pdbrc files in your home directory
and in the current directory, if they exist.  Commands supplied with
-c are executed after commands from .pdbrc files.

To let the script run until an exception occurs, use "-c continue".
To let the script run up to a given line X in the debugged file, use
"-c 'until X'".

options:
  -h, --help            show this help message and exit
  -c, --command command
                        pdb commands to execute as if given in a .pdbrc file
  -m module
  -p, --pid PID         attach to the specified PID
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/home/bswck/Python/cpython/Lib/pdb.py", line 3650, in <module>
    pdb.main()
    ~~~~~~~~^^
  File "/home/bswck/Python/cpython/Lib/pdb.py", line 3568, in main
    sys.exit(2)
    ~~~~~~~~^^^
SystemExit: 2
>>> exit

Broken output of python -i -m code

❯ ./python -i -m code
Python 3.15.0a0 (heads/main:5f91d5d9a41, Oct 12 2025, 22:21:59) [GCC 15.2.1 20250808 (Red Hat 15.2.1-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> exit()
now exiting InteractiveConsole...
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/home/bswck/Python/cpython/Lib/code.py", line 397, in <module>
    interact(banner)
    ~~~~~~~~^^^^^^^^
  File "/home/bswck/Python/cpython/Lib/code.py", line 383, in interact
    console.interact(banner, exitmsg)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/home/bswck/Python/cpython/Lib/code.py", line 287, in interact
    raise e
  File "/home/bswck/Python/cpython/Lib/code.py", line 277, in interact
    more = self.push(line)
  File "/home/bswck/Python/cpython/Lib/code.py", line 325, in push
    more = self.runsource(source, filename, symbol=_symbol)
  File "/home/bswck/Python/cpython/Lib/code.py", line 75, in runsource
    self.runcode(code)
    ~~~~~~~~~~~~^^^^^^
  File "/home/bswck/Python/cpython/Lib/code.py", line 91, in runcode
    exec(code, self.locals)
    ~~~~^^^^^^^^^^^^^^^^^^^
  File "<console>", line 1, in <module>
  File "<frozen _sitebuiltins>", line 26, in __call__
SystemExit: None
warning: can't use pyrepl: I/O operation on closed file
>>> exit()

This is an easy fix. Well, we could avoid the problem by just documenting here that combining this flag isn't supported and perhaps guard against sys.flags.interactive in custom REPLs, but I dont think it's reasonable at all in this case.

We can simply introduce a flag (like, sys._running_custom_repl) or a function (like, sys._enter_custom_repl, that could raise an error if re-entered) that would inform the pymain_run_python function back that something already acted as the REPL for this runtime. The scope of that feature would be tightly coupled to pymain_run_python only! The feature could be then used in interact() or wherever it fits the most.

I don't have a clear proposal regarding the constraints that sys._enter_custom_repl would check; that's a work in progress and I'm open to suggestions.

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Metadata

Metadata

Assignees

No one assigned

    Labels

    type-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions