https://github.com/python/cpython/commit/2a3189ce67e415c84874f2f94cf10b52928c47c0
commit: 2a3189ce67e415c84874f2f94cf10b52928c47c0
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-06-26T19:22:21Z
summary:

[3.15] gh-80937: Fix memory leak in tkinter createcommand (GH-152294) 
(GH-152327)

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 <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-06-26-13-10-00.gh-issue-80937.Hq3mNp.rst
M Lib/test/test_tkinter/test_misc.py
M Modules/_tkinter.c

diff --git a/Lib/test/test_tkinter/test_misc.py 
b/Lib/test/test_tkinter/test_misc.py
index 4a95f5550878d3..2924fa0caacc71 100644
--- a/Lib/test/test_tkinter/test_misc.py
+++ b/Lib/test/test_tkinter/test_misc.py
@@ -1,6 +1,7 @@
 import collections.abc
 import functools
 import unittest
+import weakref
 import tkinter
 from tkinter import TclError
 import enum
@@ -348,6 +349,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 86b1b2d9a534d8..137eba40a762c0 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;
     }

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to