Skip to content

Commit 8ed1f79

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 4fa86ca commit 8ed1f79

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
@@ -347,6 +348,17 @@ def callback():
347348
self.root.deletecommand(name)
348349
self.assertRaises(TclError, self.root.tk.call, name)
349350

351+
def test_createcommand_no_leak(self):
352+
# gh-80937: dropping the interpreter must release a command's callback,
353+
# even without an explicit deletecommand().
354+
interp = tkinter.Tcl()
355+
callback = lambda: ''
356+
ref = weakref.ref(callback)
357+
interp.tk.createcommand('cb', callback)
358+
del callback, interp
359+
support.gc_collect()
360+
self.assertIsNone(ref())
361+
350362
def test_option(self):
351363
self.addCleanup(self.root.option_clear)
352364
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
@@ -2431,7 +2431,7 @@ PythonCmdDelete(ClientData clientData)
24312431
PythonCmd_ClientData *data = (PythonCmd_ClientData *)clientData;
24322432

24332433
ENTER_PYTHON
2434-
Py_XDECREF(data->self);
2434+
/* data->self is borrowed. */
24352435
Py_XDECREF(data->func);
24362436
PyMem_Free(data);
24372437
LEAVE_PYTHON
@@ -2500,7 +2500,9 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name,
25002500
data = PyMem_NEW(PythonCmd_ClientData, 1);
25012501
if (!data)
25022502
return PyErr_NoMemory();
2503-
Py_INCREF(self);
2503+
/* Borrow the interpreter: a strong reference would form an uncollectable
2504+
cycle (interp -> command -> data->self -> interp) and leak the command
2505+
(gh-80937). The command cannot outlive the interpreter. */
25042506
data->self = self;
25052507
data->func = Py_NewRef(func);
25062508
if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) {
@@ -2533,6 +2535,7 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name,
25332535
}
25342536
if (err) {
25352537
PyErr_SetString(Tkinter_TclError, "can't create Tcl command");
2538+
Py_DECREF(data->func);
25362539
PyMem_Free(data);
25372540
return NULL;
25382541
}

0 commit comments

Comments
 (0)