Skip to content

Commit 2a1d898

Browse files
serhiy-storchakaclaude
authored andcommitted
gh-80937: Fix memory leak in tkinter createcommand (GH-152294)
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 bbf7786) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 668860b commit 2a1d898

3 files changed

Lines changed: 21 additions & 2 deletions

File tree

Lib/test/test_tkinter/test_misc.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import functools
22
import unittest
3+
import weakref
34
import tkinter
45
from tkinter import TclError
56
import enum
@@ -333,6 +334,17 @@ def callback():
333334
self.root.deletecommand(name)
334335
self.assertRaises(TclError, self.root.tk.call, name)
335336

337+
def test_createcommand_no_leak(self):
338+
# gh-80937: dropping the interpreter must release a command's callback,
339+
# even without an explicit deletecommand().
340+
interp = tkinter.Tcl()
341+
callback = lambda: ''
342+
ref = weakref.ref(callback)
343+
interp.tk.createcommand('cb', callback)
344+
del callback, interp
345+
support.gc_collect()
346+
self.assertIsNone(ref())
347+
336348
def test_option(self):
337349
self.addCleanup(self.root.option_clear)
338350
self.root.option_add('*Button.background', 'red')
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix a memory leak in :mod:`tkinter` when a Tcl command created with
2+
``createcommand`` was not explicitly removed before the interpreter was
3+
deleted. The command no longer keeps the interpreter alive through a
4+
reference cycle.

Modules/_tkinter.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2406,7 +2406,7 @@ PythonCmdDelete(ClientData clientData)
24062406
PythonCmd_ClientData *data = (PythonCmd_ClientData *)clientData;
24072407

24082408
ENTER_PYTHON
2409-
Py_XDECREF(data->self);
2409+
/* data->self is borrowed. */
24102410
Py_XDECREF(data->func);
24112411
PyMem_Free(data);
24122412
LEAVE_PYTHON
@@ -2475,7 +2475,9 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name,
24752475
data = PyMem_NEW(PythonCmd_ClientData, 1);
24762476
if (!data)
24772477
return PyErr_NoMemory();
2478-
Py_INCREF(self);
2478+
/* Borrow the interpreter: a strong reference would form an uncollectable
2479+
cycle (interp -> command -> data->self -> interp) and leak the command
2480+
(gh-80937). The command cannot outlive the interpreter. */
24792481
data->self = self;
24802482
data->func = Py_NewRef(func);
24812483
if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) {
@@ -2506,6 +2508,7 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name,
25062508
}
25072509
if (err) {
25082510
PyErr_SetString(Tkinter_TclError, "can't create Tcl command");
2511+
Py_DECREF(data->func);
25092512
PyMem_Free(data);
25102513
return NULL;
25112514
}

0 commit comments

Comments
 (0)