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 <storchaka@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
import functools
import unittest
+import weakref
import tkinter
from tkinter import TclError
import enum
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')
--- /dev/null
+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.
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
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()) {
}
if (err) {
PyErr_SetString(Tkinter_TclError, "can't create Tcl command");
+ Py_DECREF(data->func);
PyMem_Free(data);
return NULL;
}