diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index c2d64d63d04a1b2..b37aa4d1606ccc7 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 @@ -333,6 +334,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 000000000000000..4ea0179c6586d4f --- /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 718b378a63b0fa0..38b0b101c9b935c 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -2406,7 +2406,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 @@ -2475,7 +2475,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()) { @@ -2506,6 +2508,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; }