https://github.com/python/cpython/commit/b1679d501de294a8dfacaf311a5d522ff04d0466 commit: b1679d501de294a8dfacaf311a5d522ff04d0466 branch: 3.14 author: Miss Islington (bot) <[email protected]> committer: serhiy-storchaka <[email protected]> date: 2026-06-26T19:17:48Z summary:
[3.14] gh-80937: Fix memory leak in tkinter createcommand (GH-152294) (GH-152328) A command created with createcommand() held a strong reference to the interpreter, forming an uncollectable cycle (interpreter -> command -> interpreter) that kept the interpreter and the callback alive until the command was removed with deletecommand() or destroy(). The command now borrows the reference; it cannot outlive the interpreter, which deletes its commands when finalized. (cherry picked from commit bbf7786a904e558a15d01475356167e29b2e3708) Co-authored-by: Serhiy Storchaka <[email protected]> Co-authored-by: Claude Opus 4.8 <[email protected]> files: A Misc/NEWS.d/next/Library/2026-06-26-13-10-00.gh-issue-80937.Hq3mNp.rst M Lib/test/test_tkinter/test_misc.py M Modules/_tkinter.c diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index d4e0f67a8d52eb..d2f5e450907833 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -1,5 +1,6 @@ import functools import unittest +import weakref import tkinter from tkinter import TclError import enum @@ -347,6 +348,17 @@ def callback(): self.root.deletecommand(name) self.assertRaises(TclError, self.root.tk.call, name) + def test_createcommand_no_leak(self): + # gh-80937: dropping the interpreter must release a command's callback, + # even without an explicit deletecommand(). + interp = tkinter.Tcl() + callback = lambda: '' + ref = weakref.ref(callback) + interp.tk.createcommand('cb', callback) + del callback, interp + support.gc_collect() + self.assertIsNone(ref()) + def test_option(self): self.addCleanup(self.root.option_clear) self.root.option_add('*Button.background', 'red') diff --git a/Misc/NEWS.d/next/Library/2026-06-26-13-10-00.gh-issue-80937.Hq3mNp.rst b/Misc/NEWS.d/next/Library/2026-06-26-13-10-00.gh-issue-80937.Hq3mNp.rst new file mode 100644 index 00000000000000..4ea0179c6586d4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-26-13-10-00.gh-issue-80937.Hq3mNp.rst @@ -0,0 +1,4 @@ +Fix a memory leak in :mod:`tkinter` when a Tcl command created with +``createcommand`` was not explicitly removed before the interpreter was +deleted. The command no longer keeps the interpreter alive through a +reference cycle. diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 7e7a3ad144346b..2153acd2b56339 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -2431,7 +2431,7 @@ PythonCmdDelete(ClientData clientData) PythonCmd_ClientData *data = (PythonCmd_ClientData *)clientData; ENTER_PYTHON - Py_XDECREF(data->self); + /* data->self is borrowed. */ Py_XDECREF(data->func); PyMem_Free(data); LEAVE_PYTHON @@ -2500,7 +2500,9 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name, data = PyMem_NEW(PythonCmd_ClientData, 1); if (!data) return PyErr_NoMemory(); - Py_INCREF(self); + /* Borrow the interpreter: a strong reference would form an uncollectable + cycle (interp -> command -> data->self -> interp) and leak the command + (gh-80937). The command cannot outlive the interpreter. */ data->self = self; data->func = Py_NewRef(func); if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) { @@ -2533,6 +2535,7 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name, } if (err) { PyErr_SetString(Tkinter_TclError, "can't create Tcl command"); + Py_DECREF(data->func); PyMem_Free(data); return NULL; } _______________________________________________ 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]
