Skip to content

Commit bbf7786

Browse files
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. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9626ef8 commit bbf7786

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
@@ -3,6 +3,7 @@
33
import platform
44
import sys
55
import unittest
6+
import weakref
67
import tkinter
78
from tkinter import TclError
89
import enum
@@ -350,6 +351,17 @@ def callback():
350351
self.root.deletecommand(name)
351352
self.assertRaises(TclError, self.root.tk.call, name)
352353

354+
def test_createcommand_no_leak(self):
355+
# gh-80937: dropping the interpreter must release a command's callback,
356+
# even without an explicit deletecommand().
357+
interp = tkinter.Tcl()
358+
callback = lambda: ''
359+
ref = weakref.ref(callback)
360+
interp.tk.createcommand('cb', callback)
361+
del callback, interp
362+
support.gc_collect()
363+
self.assertIsNone(ref())
364+
353365
def test_option(self):
354366
self.addCleanup(self.root.option_clear)
355367
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
@@ -2464,7 +2464,7 @@ PythonCmdDelete(ClientData clientData)
24642464
PythonCmd_ClientData *data = (PythonCmd_ClientData *)clientData;
24652465

24662466
ENTER_PYTHON
2467-
Py_XDECREF(data->self);
2467+
/* data->self is borrowed. */
24682468
Py_XDECREF(data->func);
24692469
PyMem_Free(data);
24702470
LEAVE_PYTHON
@@ -2533,7 +2533,9 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name,
25332533
data = PyMem_NEW(PythonCmd_ClientData, 1);
25342534
if (!data)
25352535
return PyErr_NoMemory();
2536-
Py_INCREF(self);
2536+
/* Borrow the interpreter: a strong reference would form an uncollectable
2537+
cycle (interp -> command -> data->self -> interp) and leak the command
2538+
(gh-80937). The command cannot outlive the interpreter. */
25372539
data->self = self;
25382540
data->func = Py_NewRef(func);
25392541
if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) {
@@ -2566,6 +2568,7 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name,
25662568
}
25672569
if (err) {
25682570
PyErr_SetString(Tkinter_TclError, "can't create Tcl command");
2571+
Py_DECREF(data->func);
25692572
PyMem_Free(data);
25702573
return NULL;
25712574
}

0 commit comments

Comments
 (0)