https://github.com/python/cpython/commit/752e23ec9061640b57efb4fbad6859e4448e42de
commit: 752e23ec9061640b57efb4fbad6859e4448e42de
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-06-23T04:26:53Z
summary:

[3.15] gh-75666: Fix a reference leak in tkinter event bindings (GH-151808) 
(GH-151958)

The Tcl commands created for event callbacks are now deleted when a
binding is replaced or unbound, instead of being leaked.
(cherry picked from commit 3f09a175ad022ca7ccdbb8583a0c137d493533ef)

Co-authored-by: Serhiy Storchaka <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-06-20-15-00-00.gh-issue-75666.Kt9xQ2.rst
M Lib/idlelib/idle_test/test_iomenu.py
M Lib/test/test_tkinter/test_misc.py
M Lib/tkinter/__init__.py

diff --git a/Lib/idlelib/idle_test/test_iomenu.py 
b/Lib/idlelib/idle_test/test_iomenu.py
index e0642cf0cabef04..80f72bdfe5ff0ef 100644
--- a/Lib/idlelib/idle_test/test_iomenu.py
+++ b/Lib/idlelib/idle_test/test_iomenu.py
@@ -23,11 +23,10 @@ def setUpClass(cls):
         cls.root = Tk()
         cls.root.withdraw()
         cls.editwin = EditorWindow(root=cls.root)
-        cls.io = iomenu.IOBinding(cls.editwin)
+        cls.io = cls.editwin.io
 
     @classmethod
     def tearDownClass(cls):
-        cls.io.close()
         cls.editwin._close()
         del cls.editwin
         cls.root.update_idletasks()
diff --git a/Lib/test/test_tkinter/test_misc.py 
b/Lib/test/test_tkinter/test_misc.py
index 96cc7fb85929d05..fba528764415aba 100644
--- a/Lib/test/test_tkinter/test_misc.py
+++ b/Lib/test/test_tkinter/test_misc.py
@@ -1469,6 +1469,8 @@ def test3(e): pass
         self.assertNotIn(funcid, script)
         self.assertNotIn(funcid2, script)
         self.assertIn(funcid3, script)
+        self.assertCommandNotExist(funcid)
+        self.assertCommandNotExist(funcid2)
         self.assertCommandExist(funcid3)
 
     def test_bind_class(self):
@@ -1513,8 +1515,8 @@ def test2(e): pass
         unbind_class('Test', event)
         self.assertEqual(bind_class('Test', event), '')
         self.assertEqual(bind_class('Test'), ())
-        self.assertCommandExist(funcid)
-        self.assertCommandExist(funcid2)
+        self.assertCommandNotExist(funcid)
+        self.assertCommandNotExist(funcid2)
 
         unbind_class('Test', event)  # idempotent
 
@@ -1542,8 +1544,8 @@ def test3(e): pass
         self.assertNotIn(funcid, script)
         self.assertNotIn(funcid2, script)
         self.assertIn(funcid3, script)
-        self.assertCommandExist(funcid)
-        self.assertCommandExist(funcid2)
+        self.assertCommandNotExist(funcid)
+        self.assertCommandNotExist(funcid2)
         self.assertCommandExist(funcid3)
 
     def test_bind_all(self):
@@ -1585,8 +1587,8 @@ def test2(e): pass
         unbind_all(event)
         self.assertEqual(bind_all(event), '')
         self.assertNotIn(event, bind_all())
-        self.assertCommandExist(funcid)
-        self.assertCommandExist(funcid2)
+        self.assertCommandNotExist(funcid)
+        self.assertCommandNotExist(funcid2)
 
         unbind_all(event)  # idempotent
 
@@ -1614,8 +1616,8 @@ def test3(e): pass
         self.assertNotIn(funcid, script)
         self.assertNotIn(funcid2, script)
         self.assertIn(funcid3, script)
-        self.assertCommandExist(funcid)
-        self.assertCommandExist(funcid2)
+        self.assertCommandNotExist(funcid)
+        self.assertCommandNotExist(funcid2)
         self.assertCommandExist(funcid3)
 
     def _test_tag_bind(self, w):
diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py
index b0152778cb54855..6875deaf5a5cca3 100644
--- a/Lib/tkinter/__init__.py
+++ b/Lib/tkinter/__init__.py
@@ -1503,13 +1503,26 @@ def bindtags(self, tagList=None):
         else:
             self.tk.call('bindtags', self._w, tagList)
 
-    def _bind(self, what, sequence, func, add, needcleanup=1):
+    def _delete_bind_commands(self, *what):
+        lines = self.tk.call(what).split('\n')
+        p = re.compile(r'if \{"\[([^ ]+) .*\]" == "break"\} break')
+        for line in lines:
+            m = p.fullmatch(line)
+            if m:
+                funcid = m[1]
+                try:
+                    self.deletecommand(funcid)
+                except TclError:
+                    pass
+
+    def _bind(self, what, sequence, func, add):
         """Internal function."""
         if isinstance(func, str):
             self.tk.call(what + (sequence, func))
         elif func:
-            funcid = self._register(func, self._substitute,
-                        needcleanup)
+            if not add:
+                self._delete_bind_commands(*what, sequence)
+            funcid = self._register(func, self._substitute, needcleanup=True)
             cmd = ('%sif {"[%s %s]" == "break"} break\n'
                    %
                    (add and '+' or '',
@@ -1575,6 +1588,7 @@ def unbind(self, sequence, funcid=None):
 
     def _unbind(self, what, funcid=None):
         if funcid is None:
+            self._delete_bind_commands(*what)
             self.tk.call(*what, '')
         else:
             lines = self.tk.call(what).split('\n')
@@ -1591,7 +1605,7 @@ def bind_all(self, sequence=None, func=None, add=None):
         An additional boolean parameter ADD specifies whether FUNC will
         be called additionally to the other bound function or whether
         it will replace the previous function. See bind for the return 
value."""
-        return self._root()._bind(('bind', 'all'), sequence, func, add, True)
+        return self._root()._bind(('bind', 'all'), sequence, func, add)
 
     def unbind_all(self, sequence):
         """Unbind for all widgets for event SEQUENCE all functions."""
@@ -1605,7 +1619,7 @@ def bind_class(self, className, sequence=None, func=None, 
add=None):
         whether it will replace the previous function. See bind for
         the return value."""
 
-        return self._root()._bind(('bind', className), sequence, func, add, 
True)
+        return self._root()._bind(('bind', className), sequence, func, add)
 
     def unbind_class(self, className, sequence):
         """Unbind for all widgets with bindtag CLASSNAME for event SEQUENCE
diff --git 
a/Misc/NEWS.d/next/Library/2026-06-20-15-00-00.gh-issue-75666.Kt9xQ2.rst 
b/Misc/NEWS.d/next/Library/2026-06-20-15-00-00.gh-issue-75666.Kt9xQ2.rst
new file mode 100644
index 000000000000000..d2b2b066837bb1f
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-06-20-15-00-00.gh-issue-75666.Kt9xQ2.rst
@@ -0,0 +1,2 @@
+Fix a reference leak in :mod:`tkinter`: the Tcl commands created for event
+callbacks are now deleted when a binding is replaced or unbound.

_______________________________________________
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