Bug report
Bug description:
The following script causes the event loop to spin when Python is in interactive mode.
To reproduce this bug, run this script as python -i script.py. Then press the top button to call the root.wait_variable command. Once waiting, press "enter" in the command window. Monitoring top will show you that CPU usage goes up to 100%.
The reason is that EventHook (in _tkinter.c) calls Tcl_CreateFileHandler to ask Tcl to monitor stdin.
root.wait_variable calls Tk_TkwaitObjCmd, which will sit in the following tight event loop (in generic/tkCmds.c in the Tk source):
done = 0; while (!done) { if (Tcl_Canceled(interp, TCL_LEAVE_ERR_MSG) == TCL_ERROR) { code = TCL_ERROR; break; } Tcl_DoOneEvent(0); }
But because this event loop starts before EventHook exits, Tcl is still monitoring stdin.
Therefore, Tcl_DoOneEvent detects activity on stdin, resulting in a call to MyFileProc (in _tkinter.c), which duly sets stdin_ready to 1 without reading the input on stdin. But the tight event loop above doesn't know about stdin_ready, and therefore keeps running, calling Tcl_DoOneEvent again, which finds the same input still available on stdin. Thus this tight event loop keeps spinning, causing the CPU usage to go up to 100%.
Note that calling trigger_wait_variable from the Python command prompt (instead of pressing the button) will not result in a spinning event loop, as in that case EventHook exits first, calling Tcl_DeleteFileHandler on stdin before trigger_wait_variable is executed. Therefore in that case Tcl is no longer monitoring stdin when entering the tight event loop above.
This was tested on Linux.
import tkinter root = tkinter.Tk() root.title("wait_variable Demo") py_var = tkinter.StringVar() label = tkinter.Label(root, text="click the top button to start waiting", padx=20, pady=10) label.pack() def trigger_wait_variable(): label.config(text="click the bottom button to stop waiting") print("click the bottom button to stop waiting") root.wait_variable(py_var) # blocks until py_var is set print("finished waiting") label.config(text=f"finished waiting") tkinter.Button(root, text="call wait_variable to start waiting", command=trigger_wait_variable).pack() def set_py_var(): py_var.set("done from wait_variable button") tkinter.Button(root, text="stop waiting", command=set_py_var).pack()
CPython versions tested on:
3.12
Operating systems tested on:
Linux
Linked PRs
- gh-139145: fix spinning event loop in tkinter #139180
- gh-139145: Fix tkinter event loop in interactive mode #152257
- [3.15] gh-139145: Fix tkinter event loop in interactive mode (GH-152257) #152291
- [3.14] gh-139145: Fix tkinter event loop in interactive mode (GH-152257) #152292
- [3.13] gh-139145: Fix tkinter event loop in interactive mode (GH-152257) #152293