https://github.com/python/cpython/commit/e2b711c950b702c965adb0349bf9d7aa3ffc0bd0
commit: e2b711c950b702c965adb0349bf9d7aa3ffc0bd0
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: terryjreedy <[email protected]>
date: 2026-10-09T17:17:29-04:00
summary:

[3.15] gh-69919: Catch all compile errors in the code module, pyrepl and IDLE 
(GH-157585) (#158025)

gh-69919: Catch all compile errors in the code module, pyrepl and IDLE 
(GH-157585)

compile() can raise MemoryError or RecursionError for too deeply
nested source, not only SyntaxError, OverflowError and ValueError.
IDLE's Shell then lost its prompt until the input was deleted.
(cherry picked from commit 4bc392c13462096213777c6b2bdc470028c41536)

Co-authored-by: Serhiy Storchaka <[email protected]>
Co-authored-by: Terry Jan Reedy <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>

files:
A Misc/NEWS.d/next/IDLE/2026-09-15-16-37-19.gh-issue-69919.f6AamT.rst
A Misc/NEWS.d/next/Library/2026-09-15-16-37-19.gh-issue-69919.izH1zP.rst
M Doc/builtins/functions.rst
M Doc/library/code.rst
M Lib/_pyrepl/console.py
M Lib/_pyrepl/simple_interact.py
M Lib/code.py
M Lib/idlelib/idle_test/test_runscript.py
M Lib/idlelib/pyshell.py
M Lib/idlelib/runscript.py
M Lib/pdb.py
M Lib/test/test_code_module.py
M Lib/test/test_pdb.py
M Lib/test/test_pyrepl/test_interact.py

diff --git a/Doc/builtins/functions.rst b/Doc/builtins/functions.rst
index f8d848791f6cdb..88717a35745204 100644
--- a/Doc/builtins/functions.rst
+++ b/Doc/builtins/functions.rst
@@ -344,8 +344,14 @@ are always available.  They are listed here in 
alphabetical order.
    It is needed to unambiguous :ref:`filter <warning-filter>` syntax warnings
    by module name.
 
-   This function raises :exc:`SyntaxError` or :exc:`ValueError` if the compiled
-   source is invalid.
+   This function raises :exc:`SyntaxError` if the compiled source is invalid,
+   including a *source* containing a null character or that cannot be decoded;
+   :exc:`ValueError` if *mode* or *flags* is invalid,
+   or if a string *source* contains surrogate characters;
+   :exc:`MemoryError` or :exc:`RecursionError` if *source* is too complex
+   to parse or compile,
+   for example an expression with many thousands of nested operators;
+   and :exc:`OverflowError` if *source* is too large.
 
    If you want to parse Python code into its AST representation, see
    :func:`ast.parse`.
@@ -377,11 +383,15 @@ are always available.  They are listed here in 
alphabetical order.
       Previously, :exc:`TypeError` was raised when null bytes were encountered
       in *source*.
 
-   .. versionadded:: 3.8
+   .. versionchanged:: 3.8
       ``ast.PyCF_ALLOW_TOP_LEVEL_AWAIT`` can now be passed in flags to enable
       support for top-level ``await``, ``async for``, and ``async with``.
 
-   .. versionadded:: 3.15
+   .. versionchanged:: 3.12
+      :exc:`SyntaxError` is raised instead of :exc:`ValueError` when null bytes
+      are encountered in *source*.
+
+   .. versionchanged:: 3.15
       Added the *module* parameter.
 
 
diff --git a/Doc/library/code.rst b/Doc/library/code.rst
index 59c016d21501b0..71eb3e8646cd01 100644
--- a/Doc/library/code.rst
+++ b/Doc/library/code.rst
@@ -92,7 +92,7 @@ Interactive Interpreter Objects
    *symbol* is ``'single'``.  One of several things can happen:
 
    * The input is incorrect; :func:`compile_command` raised an exception
-     (:exc:`SyntaxError` or :exc:`OverflowError`).  A syntax traceback will be
+     (usually :exc:`SyntaxError`).  A syntax traceback will be
      printed by calling the :meth:`showsyntaxerror` method.  :meth:`runsource`
      returns ``False``.
 
diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py
index 2a53d5ff581fa2..70b5757393adcd 100644
--- a/Lib/_pyrepl/console.py
+++ b/Lib/_pyrepl/console.py
@@ -261,7 +261,7 @@ def runsource(self, source, filename="<input>", 
symbol="single"):
                 )
             self.showsyntaxerror(filename, source=source)
             return False
-        except (OverflowError, ValueError):
+        except Exception:
             self.showsyntaxerror(filename, source=source)
             return False
         if tree.body:
@@ -282,7 +282,7 @@ def runsource(self, source, filename="<input>", 
symbol="single"):
                     )
                 self.showsyntaxerror(filename, source=source)
                 return False
-            except (OverflowError, ValueError):
+            except Exception:
                 self.showsyntaxerror(filename, source=source)
                 return False
 
diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py
index c169d0191bd833..176071d9b1a345 100644
--- a/Lib/_pyrepl/simple_interact.py
+++ b/Lib/_pyrepl/simple_interact.py
@@ -83,7 +83,7 @@ def _more_lines(console: code.InteractiveConsole, 
unicodetext: str) -> bool:
     src = _strip_final_indent(unicodetext)
     try:
         code = console.compile(src, "<stdin>", "single")
-    except (OverflowError, SyntaxError, ValueError):
+    except Exception:
         lines = src.splitlines(keepends=True)
         if len(lines) == 1:
             return False
diff --git a/Lib/code.py b/Lib/code.py
index df1d7199e33934..f658049197e1b7 100644
--- a/Lib/code.py
+++ b/Lib/code.py
@@ -44,8 +44,8 @@ def runsource(self, source, filename="<input>", 
symbol="single"):
         One of several things can happen:
 
         1) The input is incorrect; compile_command() raised an
-        exception (SyntaxError or OverflowError).  A syntax traceback
-        will be printed by calling the showsyntaxerror() method.
+        exception (usually SyntaxError).  A syntax traceback will be
+        printed by calling the showsyntaxerror() method.
 
         2) The input is incomplete, and more input is required;
         compile_command() returned None.  Nothing happens.
@@ -62,7 +62,7 @@ def runsource(self, source, filename="<input>", 
symbol="single"):
         """
         try:
             code = self.compile(source, filename, symbol)
-        except (OverflowError, SyntaxError, ValueError):
+        except Exception:
             # Case 1
             self.showsyntaxerror(filename, source=source)
             return False
diff --git a/Lib/idlelib/idle_test/test_runscript.py 
b/Lib/idlelib/idle_test/test_runscript.py
index 1e47f402d504f9..69d253b8f2c73f 100644
--- a/Lib/idlelib/idle_test/test_runscript.py
+++ b/Lib/idlelib/idle_test/test_runscript.py
@@ -29,6 +29,19 @@ def test_init(self):
         sb = runscript.ScriptBinding(ew)
         ew._close()
 
+    def test_checksyntax_compile_error(self):
+        # gh-69919: any error raised by compile() is reported.
+        ew = EditorWindow(root=self.root)
+        sb = runscript.ScriptBinding(ew)
+        sb.flist = mock.Mock()
+        sb.errorbox = mock.Mock()
+        with (mock.patch('idlelib.runscript.compile', create=True,
+                         side_effect=MemoryError()),
+              mock.patch('idlelib.runscript.open', 
mock.mock_open(read_data=b'x\n'))):
+            self.assertFalse(sb.checksyntax('test.py'))
+        sb.errorbox.assert_called_once_with('MemoryError', '<no detail 
available>')
+        ew._close()
+
     def test_run_module_event_shell_busy_no_restart(self):
         # gh-82183: running without restarting the busy shell aborts.
         ew = EditorWindow(root=self.root)
diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py
index dbcc9be643de29..338cb7f569dba5 100755
--- a/Lib/idlelib/pyshell.py
+++ b/Lib/idlelib/pyshell.py
@@ -686,7 +686,7 @@ def execfile(self, filename, source=None):
                               + source + "\ndel __file__")
         try:
             code = compile(source, filename, "exec")
-        except (OverflowError, SyntaxError):
+        except Exception:
             self.tkconsole.resetoutput()
             print('*** Error in script or command!\n'
                  'Traceback (most recent call last):',
@@ -737,19 +737,23 @@ def showsyntaxerror(self, filename=None, **kwargs):
         tkconsole = self.tkconsole
         text = tkconsole.text
         type, value, tb = sys.exc_info()
-        msg = getattr(value, 'msg', '') or value or "<no detail available>"
-        lineno = getattr(value, 'lineno', '') or 1
-        offset = getattr(value, 'offset', '') or 0
+        if not issubclass(type, SyntaxError):
+            tkconsole.resetoutput()
+            InteractiveInterpreter.showsyntaxerror(self, filename, **kwargs)
+            tkconsole.showprompt()
+            return
+        msg = value.msg or "<no detail available>"
+        lineno = value.lineno or 1
+        offset = value.offset or 0
         if offset == 0:
             lineno += 1 #mark end of offending line
         if lineno == 1:
-            pos = "iomark + %d chars" % (offset-1)
+            pos = f"iomark + {offset-1} chars"
         else:
-            pos = "iomark linestart + %d lines + %d chars" % \
-                  (lineno-1, offset-1)
+            pos = f"iomark linestart + {lineno-1} lines + {offset-1} chars"
         tkconsole.colorize_syntax_error(text, pos)
         tkconsole.resetoutput()
-        self.write("SyntaxError: %s\n" % msg)
+        self.write(f"{type.__name__}: {msg}\n")
         tkconsole.showprompt()
 
     def showtraceback(self):
diff --git a/Lib/idlelib/runscript.py b/Lib/idlelib/runscript.py
index cd52d206c9ca0f..af3581ce0ce773 100644
--- a/Lib/idlelib/runscript.py
+++ b/Lib/idlelib/runscript.py
@@ -93,15 +93,19 @@ def checksyntax(self, filename):
         try:
             # If successful, return the compiled code
             return compile(source, filename, "exec")
-        except (SyntaxError, OverflowError, ValueError) as value:
-            msg = getattr(value, 'msg', '') or value or "<no detail available>"
-            lineno = getattr(value, 'lineno', '') or 1
-            offset = getattr(value, 'offset', '') or 0
+        except SyntaxError as value:
+            msg = value.msg or "<no detail available>"
+            lineno = value.lineno or 1
+            offset = value.offset or 0
             if offset == 0:
                 lineno += 1  #mark end of offending line
             pos = "0.0 + %d lines + %d chars" % (lineno-1, offset-1)
             editwin.colorize_syntax_error(text, pos)
-            self.errorbox("SyntaxError", "%-20s" % msg)
+            self.errorbox(type(value).__name__, msg)
+            return False
+        except Exception as value:
+            msg = str(value) or "<no detail available>"
+            self.errorbox(type(value).__name__, msg)
             return False
         finally:
             shell.set_warning_stream(saved_stream)
diff --git a/Lib/pdb.py b/Lib/pdb.py
index e0c9b4d2c241a7..75fca193d6e2a4 100644
--- a/Lib/pdb.py
+++ b/Lib/pdb.py
@@ -155,7 +155,7 @@ def find_function(funcname, filename):
             if funcdef:
                 try:
                     code = compile(funcdef, filename, 'exec')
-                except SyntaxError:
+                except Exception:
                     continue
                 # We should always be able to find the code object here
                 funccode = next(c for c in code.co_consts if
@@ -2776,7 +2776,7 @@ def _compile_error_message(self, expr):
         """Return the error message as string if compiling `expr` fails."""
         try:
             compile(expr, "<stdin>", "eval")
-        except SyntaxError as exc:
+        except Exception as exc:
             return _rstr(self._format_exc(exc))
         return ""
 
diff --git a/Lib/test/test_code_module.py b/Lib/test/test_code_module.py
index 3642b47c2c1f03..c0c3454a97fbd8 100644
--- a/Lib/test/test_code_module.py
+++ b/Lib/test/test_code_module.py
@@ -140,6 +140,17 @@ def test_unicode_error(self):
         self.assertIsNone(self.sysmod.last_value.__traceback__)
         self.assertIs(self.sysmod.last_exc, self.sysmod.last_value)
 
+    def test_compile_error(self):
+        # Any error raised by compile() must be reported (gh-69919).
+        self.infunc.side_effect = ['-' * 100_000 + '1', EOFError('Finished')]
+        self.console.interact()
+        output = ''.join(''.join(call[1]) for call in self.stderr.method_calls)
+        output = output[output.index('(InteractiveConsole)'):]
+        output = output[output.index('\n') + 1:]
+        self.assertRegex(output, r'^(MemoryError|RecursionError): ')
+        self.assertIn(self.sysmod.last_type, (MemoryError, RecursionError))
+        self.assertIs(self.sysmod.last_exc, self.sysmod.last_value)
+
     def test_sysexcepthook(self):
         self.infunc.side_effect = ["def f():",
                                    "    raise ValueError('BOOM!')",
diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py
index cfddd96a24fd7d..5f2d3f87ba2499 100644
--- a/Lib/test/test_pdb.py
+++ b/Lib/test/test_pdb.py
@@ -3653,6 +3653,23 @@ def quux():
             ('bœr', 5),
         )
 
+    def test_find_function_too_complex(self):
+        # gh-69919: compile() can raise more than SyntaxError.
+        self._assert_find_function(
+            b"def foo():\n    return " + b"-" * 100_000 + b"1\n"
+            b"def bar():\n    pass\n",
+            'bar',
+            ('bar', 4),
+        )
+
+    def test_compile_error_message(self):
+        p = pdb.Pdb()
+        self.assertEqual(p._compile_error_message('1 + 1'), '')
+        self.assertIn('SyntaxError', p._compile_error_message('1 +'))
+        # gh-69919: compile() can raise more than SyntaxError.
+        self.assertRegex(p._compile_error_message('-' * 100_000 + '1'),
+                         r'^(MemoryError|RecursionError|SyntaxError):')
+
     def test_print_stack_entry_uses_dynamic_line_prefix(self):
         """Test that pdb.line_prefix binding is dynamic (gh-141781)."""
         stdout = io.StringIO()
diff --git a/Lib/test/test_pyrepl/test_interact.py 
b/Lib/test/test_pyrepl/test_interact.py
index fd4530ebc004aa..673827608abc47 100644
--- a/Lib/test/test_pyrepl/test_interact.py
+++ b/Lib/test/test_pyrepl/test_interact.py
@@ -131,6 +131,17 @@ def 
test_runsource_shows_syntax_error_for_failed_compilation(self):
             console.runsource(source)
             mock_showsyntaxerror.assert_called_once()
 
+    @force_not_colorized
+    def test_runsource_compile_error(self):
+        # Any error raised by compile() is reported (gh-69919).
+        console = InteractiveColoredConsole()
+        source = '-' * 100_000 + '1'
+        f = io.StringIO()
+        with contextlib.redirect_stderr(f):
+            result = console.runsource(source)
+        self.assertFalse(result)
+        self.assertRegex(f.getvalue(), r'^(MemoryError|RecursionError): ')
+
     def test_runsource_survives_null_bytes(self):
         console = InteractiveColoredConsole()
         source = "\x00\n"
@@ -182,6 +193,12 @@ def test_invalid_syntax_single_line(self):
         console = InteractiveColoredConsole(namespace, filename="<stdin>")
         self.assertFalse(_more_lines(console, code))
 
+    def test_compile_error_single_line(self):
+        namespace = {}
+        code = '-' * 100_000 + '1'  # MemoryError or RecursionError
+        console = InteractiveColoredConsole(namespace, filename="<stdin>")
+        self.assertFalse(_more_lines(console, code))
+
     def test_empty_line(self):
         namespace = {}
         code = ""
diff --git 
a/Misc/NEWS.d/next/IDLE/2026-09-15-16-37-19.gh-issue-69919.f6AamT.rst 
b/Misc/NEWS.d/next/IDLE/2026-09-15-16-37-19.gh-issue-69919.f6AamT.rst
new file mode 100644
index 00000000000000..fc909152113077
--- /dev/null
+++ b/Misc/NEWS.d/next/IDLE/2026-09-15-16-37-19.gh-issue-69919.f6AamT.rst
@@ -0,0 +1,2 @@
+IDLE now reports any exception raised by compiling the source, such as
+:exc:`MemoryError` for too deeply nested source, instead of hanging the Shell.
diff --git 
a/Misc/NEWS.d/next/Library/2026-09-15-16-37-19.gh-issue-69919.izH1zP.rst 
b/Misc/NEWS.d/next/Library/2026-09-15-16-37-19.gh-issue-69919.izH1zP.rst
new file mode 100644
index 00000000000000..87fd4a7aedee84
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-15-16-37-19.gh-issue-69919.izH1zP.rst
@@ -0,0 +1,4 @@
+:meth:`code.InteractiveInterpreter.runsource` and the REPL now report any
+exception raised by compiling the source, such as :exc:`MemoryError` or
+:exc:`RecursionError` for too deeply nested source, instead of propagating
+it or printing a traceback of the REPL internals.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to