https://github.com/python/cpython/commit/0632698c66bcd08e6e09cdb107ed7aff7a5258b1
commit: 0632698c66bcd08e6e09cdb107ed7aff7a5258b1
branch: 3.13
author: Serhiy Storchaka <[email protected]>
committer: terryjreedy <[email protected]>
date: 2026-09-23T21:29:39-04:00
summary:

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

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: 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 bf2c8d70120f2d7..337dfc49f2acf47 100644
--- a/Doc/builtins/functions.rst
+++ b/Doc/builtins/functions.rst
@@ -336,8 +336,14 @@ are always available.  They are listed here in 
alphabetical order.
    ``__debug__`` is true), ``1`` (asserts are removed, ``__debug__`` is false)
    or ``2`` (docstrings are removed too).
 
-   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`.
@@ -369,10 +375,14 @@ 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``.
 
+   .. versionchanged:: 3.12
+      :exc:`SyntaxError` is raised instead of :exc:`ValueError` when null bytes
+      are encountered in *source*.
+
 
 .. class:: complex(number=0, /)
            complex(string, /)
diff --git a/Doc/library/code.rst b/Doc/library/code.rst
index 52587c4dd8f8e87..f58135e91a00779 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 8956fb1242e52a4..a959ee36773a137 100644
--- a/Lib/_pyrepl/console.py
+++ b/Lib/_pyrepl/console.py
@@ -195,7 +195,7 @@ def runsource(self, source, filename="<input>", 
symbol="single"):
                 ast.PyCF_ONLY_AST,
                 incomplete_input=False,
             )
-        except (SyntaxError, OverflowError, ValueError):
+        except Exception:
             self.showsyntaxerror(filename, source=source)
             return False
         if tree.body:
@@ -216,7 +216,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 8fb2359fb51e8a8..6c0ce649068b877 100644
--- a/Lib/_pyrepl/simple_interact.py
+++ b/Lib/_pyrepl/simple_interact.py
@@ -87,7 +87,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 2777c3111877c2f..efc5924ef9c8bb8 100644
--- a/Lib/code.py
+++ b/Lib/code.py
@@ -45,8 +45,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.
@@ -63,7 +63,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 1e47f402d504f9d..69d253b8f2c73f1 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 32fd30578594ddb..30005e25d57891b 100755
--- a/Lib/idlelib/pyshell.py
+++ b/Lib/idlelib/pyshell.py
@@ -693,7 +693,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):',
@@ -743,19 +743,23 @@ def showsyntaxerror(self, filename=None, **kwargs):
         text = tkconsole.text
         text.tag_remove("ERROR", "1.0", "end")
         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 cd52d206c9ca0f3..af3581ce0ce773f 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 5c9be23e3265671..24e7f3a2ef89975 100755
--- a/Lib/pdb.py
+++ b/Lib/pdb.py
@@ -140,7 +140,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
@@ -2270,7 +2270,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 faa0b38f8373e3d..8d94dbfdb6cef17 100644
--- a/Lib/test/test_code_module.py
+++ b/Lib/test/test_code_module.py
@@ -112,6 +112,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 295d01184f5908b..7e106e4f7baf5d0 100644
--- a/Lib/test/test_pdb.py
+++ b/Lib/test/test_pdb.py
@@ -3118,6 +3118,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_find_function_found_with_encoding_cookie(self):
         self._assert_find_function(
             """\
diff --git a/Lib/test/test_pyrepl/test_interact.py 
b/Lib/test/test_pyrepl/test_interact.py
index 2041a35c8a472bf..59d9bbb0868c7b6 100644
--- a/Lib/test/test_pyrepl/test_interact.py
+++ b/Lib/test/test_pyrepl/test_interact.py
@@ -130,6 +130,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"
@@ -181,6 +192,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 000000000000000..fc9091521130770
--- /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 000000000000000..87fd4a7aedee846
--- /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