https://github.com/python/cpython/commit/3f09a175ad022ca7ccdbb8583a0c137d493533ef
commit: 3f09a175ad022ca7ccdbb8583a0c137d493533ef
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-06-23T06:59:25+03:00
summary:
gh-75666: Fix a reference leak in tkinter event bindings (GH-151808)
The Tcl commands created for event callbacks are now deleted when a
binding is replaced or unbound, instead of being leaked.
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 15239efd83904fd..14f741e802535f3 100644
--- a/Lib/test/test_tkinter/test_misc.py
+++ b/Lib/test/test_tkinter/test_misc.py
@@ -1564,6 +1564,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):
@@ -1608,8 +1610,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
@@ -1637,8 +1639,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):
@@ -1680,8 +1682,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
@@ -1709,8 +1711,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 b1015d2d5d0c23f..f940216253dee1e 100644
--- a/Lib/tkinter/__init__.py
+++ b/Lib/tkinter/__init__.py
@@ -1579,13 +1579,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 '',
@@ -1651,6 +1664,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')
@@ -1667,7 +1681,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."""
@@ -1681,7 +1695,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]