https://github.com/python/cpython/commit/6f103fab178c07cbb5f701b8ad97e275b6eb6c4c
commit: 6f103fab178c07cbb5f701b8ad97e275b6eb6c4c
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-06-30T12:19:43+03:00
summary:

gh-50966: Fix unbounded recursion in turtle drag handlers (GH-152626)

TurtleScreenBase._update() redraws with cv.update(), which also reprocesses
input events, so a handler that moves the turtle (such as
screen.ondrag(turtle.goto)) reenters _update() for every queued event until
the interpreter crashes.  A reentrant _update() now only flushes drawing with
update_idletasks().

Co-authored-by: Claude Opus 4.8 <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-06-29-22-16-57.gh-issue-50966.Tq5mLp.rst
M Lib/test/test_turtle.py
M Lib/turtle.py

diff --git a/Lib/test/test_turtle.py b/Lib/test/test_turtle.py
index 12d2eed874148c..c49ce9cdb6f6d9 100644
--- a/Lib/test/test_turtle.py
+++ b/Lib/test/test_turtle.py
@@ -577,6 +577,28 @@ def test_no_animation_calls_update_at_exit(self):
             s.update.assert_not_called()
         s.update.assert_called_once()
 
+    def test_update_is_not_reentrant(self):
+        # ondrag(goto) reenters _update() while cv.update() processes events;
+        # without a guard this recurses without bound (gh-50966).
+        s = turtle.TurtleScreen(cv=unittest.mock.MagicMock())
+        depth = max_depth = 0
+
+        def reenter():
+            nonlocal depth, max_depth
+            depth += 1
+            max_depth = max(max_depth, depth)
+            if depth < 50:
+                s._update()  # as an event handler would
+            depth -= 1
+
+        s.cv.update.reset_mock()  # ignore calls made during construction
+        s.cv.update.side_effect = reenter
+        s._update()
+        # cv.update() runs once; reentrant calls only flush idle tasks.
+        self.assertEqual(s.cv.update.call_count, 1)
+        self.assertEqual(max_depth, 1)
+        self.assertTrue(s.cv.update_idletasks.called)
+
 
 class TestTurtle(unittest.TestCase):
     def setUp(self):
diff --git a/Lib/turtle.py b/Lib/turtle.py
index ae5be9fe7039d5..af2c7448a0af17 100644
--- a/Lib/turtle.py
+++ b/Lib/turtle.py
@@ -486,6 +486,7 @@ def __init__(self, cv):
         self.canvwidth = w
         self.canvheight = h
         self.xscale = self.yscale = 1.0
+        self._updating = False
 
     def _createpoly(self):
         """Create an invisible polygon item on canvas self.cv)
@@ -555,7 +556,16 @@ def _delete(self, item):
     def _update(self):
         """Redraw graphics items on canvas
         """
-        self.cv.update()
+        if self._updating:
+            # Reentrant call (e.g. a drag handler moving the turtle,
+            # gh-50966): flush drawing without reprocessing input.
+            self.cv.update_idletasks()
+            return
+        self._updating = True
+        try:
+            self.cv.update()
+        finally:
+            self._updating = False
 
     def _delay(self, delay):
         """Delay subsequent canvas actions for delay ms."""
diff --git 
a/Misc/NEWS.d/next/Library/2026-06-29-22-16-57.gh-issue-50966.Tq5mLp.rst 
b/Misc/NEWS.d/next/Library/2026-06-29-22-16-57.gh-issue-50966.Tq5mLp.rst
new file mode 100644
index 00000000000000..96a0938c02d226
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-06-29-22-16-57.gh-issue-50966.Tq5mLp.rst
@@ -0,0 +1,3 @@
+Fix unbounded recursion in :mod:`turtle` when a mouse event handler that moves
+the turtle is reentered while the screen is being redrawn, for example with
+``screen.ondrag(turtle.goto)``.  This could previously crash the interpreter.

_______________________________________________
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