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

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

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 73dff173fb60c71..3951e716a5a0ffd 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 6508f0233b963ad..e45a46a08e1a33b 100644
--- a/Lib/_pyrepl/simple_interact.py
+++ b/Lib/_pyrepl/simple_interact.py
@@ -84,7 +84,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 b134886dc267fbd..6614de35d4daec7 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 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 7527163d8b60ebf..19a6569ac61182e 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 7dcd0cb1b37e71d..816e7bab6f329ee 100644
--- a/Lib/pdb.py
+++ b/Lib/pdb.py
@@ -151,7 +151,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
@@ -2572,7 +2572,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 3642b47c2c1f03f..c0c3454a97fbd8c 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 e51488f8e03b8e7..5a4e612c2e779a6 100644
--- a/Lib/test/test_pdb.py
+++ b/Lib/test/test_pdb.py
@@ -3609,6 +3609,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 f0837ee94e9beb4..bfd9c3f4e05097c 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 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