diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index 4c003e697d23b86..a93f5dee349e648 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -2,6 +2,7 @@ import functools import platform import sys +import textwrap import unittest import weakref import tkinter @@ -9,6 +10,7 @@ import enum from test import support from test.support import os_helper +from test.support.script_helper import assert_python_ok from test.test_tkinter.support import setUpModule # noqa: F401 from test.test_tkinter.support import (AbstractTkTest, AbstractDefaultRootTest, requires_tk, get_tk_patchlevel, @@ -53,6 +55,33 @@ class Button2(tkinter.Button): b4 = Button2(f2) self.assertEqual(len({str(b), str(b2), str(b3), str(b4)}), 4) + def test_dealloc_in_wrong_thread(self): + # gh-83274: deallocating the interpreter in the wrong thread must not + # crash. + script = textwrap.dedent(""" + import threading + import tkinter + root = tkinter.Tk() + root.destroy() + # Let another thread drop the last reference. + ready = threading.Event() + t = threading.Thread(target=lambda obj: ready.wait(), args=(root,)) + t.start() + del root + ready.set() + t.join() + print('ok') + """) + rc, out, err = assert_python_ok('-c', script) + self.assertEqual(out.strip(), b'ok') + if not support.Py_GIL_DISABLED: + # On the free-threaded build the interpreter may instead be + # deallocated in its own thread (deferred reference counting), so + # the warning is not necessarily emitted. The crucial guarantee -- + # no crash -- is already checked by assert_python_ok() above. + self.assertIn(b'RuntimeWarning', err) + self.assertIn(b'gh-83274', err) + @requires_tk(8, 6, 6) def test_tk_busy(self): root = self.root diff --git a/Misc/NEWS.d/next/Library/2026-06-26-16-30-00.gh-issue-83274.Kx9mQv.rst b/Misc/NEWS.d/next/Library/2026-06-26-16-30-00.gh-issue-83274.Kx9mQv.rst new file mode 100644 index 000000000000000..3b722d2176be920 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-26-16-30-00.gh-issue-83274.Kx9mQv.rst @@ -0,0 +1,3 @@ +Deallocating a :mod:`tkinter` application from a thread other than the one it +was created in no longer crashes the interpreter. The underlying Tcl +interpreter is leaked instead, and a :exc:`RuntimeWarning` is reported. diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 137eba40a762c0a..ed086d590d98d77 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -3132,10 +3132,24 @@ Tkapp_Dealloc(PyObject *op) { TkappObject *self = TkappObject_CAST(op); PyTypeObject *tp = Py_TYPE(self); - /*CHECK_TCL_APPARTMENT;*/ - ENTER_TCL - Tcl_DeleteInterp(Tkapp_Interp(self)); - LEAVE_TCL + if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) { + /* Deleting the interpreter from another thread aborts the process + ("Tcl_AsyncDelete: async handler deleted by the wrong thread"). + Leak it instead (gh-83274). */ + if (PyErr_WarnEx(PyExc_RuntimeWarning, + "the Tcl interpreter is leaked because it was " + "deallocated in a thread other than the one it was " + "created in (see gh-83274)", 1) < 0) + { + PyErr_FormatUnraisable("Exception ignored while finalizing " + "a Tcl interpreter"); + } + } + else { + ENTER_TCL + Tcl_DeleteInterp(Tkapp_Interp(self)); + LEAVE_TCL + } Py_XDECREF(self->trace); PyObject_Free(self); Py_DECREF(tp);