diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index 98af4d822dadc4..337a37189047ac 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -3,6 +3,7 @@ import platform import sys import unittest +import weakref import tkinter from tkinter import TclError import enum @@ -350,6 +351,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 466e275c5cecf0..c241cf15401773 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -2464,7 +2464,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 @@ -2533,7 +2533,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()) { @@ -2566,6 +2568,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; }