Skip to content

Handling the Close Button in Python GUIs- Fix Hangs and Crashes

In a community I participate in, a developer recently ran into a frustrating issue with their GUI application. They asked: “I’ve built a Python app with Tkinter, but when I click the ‘X’ button in the title bar, the window disappears but the script keeps running in my terminal. If I have a loop running, it never stops. How do I make the close button actually terminate everything?”

This is a classic “zombie process” problem in Python GUI development. By default, the window manager’s close signal might hide the window or stop the main event loop, but it doesn’t necessarily kill background threads or trigger cleanup logic.

The user provided a snippet where they were running a while True loop inside a Tkinter app. When they clicked the close button (the “X”), the GUI window vanished, but the console continued printing output from the loop, forcing them to use Ctrl+C to kill the process.

If you are using Tkinter, the most direct way to intercept the close button and ensure a clean exit is using the protocol method.

# Python 3.10+, Tkinter
import tkinter as tk
from tkinter import messagebox
def on_closing():
if messagebox.askokcancel("Quit", "Do you want to quit?"):
root.destroy() # Explicitly destroy the widget hierarchy
# root.quit() # Optional: stops the mainloop
root = tk.Tk()
root.title("Clean Exit Example")
# This binds the Window Manager 'Close' button to our function
root.protocol("WM_DELETE_WINDOW", on_closing)
root.mainloop()

Detailed Explanation: Why the “X” Button Fails

Section titled “Detailed Explanation: Why the “X” Button Fails”

In GUI programming, the “X” button on the window frame is managed by the Operating System’s Window Manager (WM). When you click it, the WM sends a signal (like WM_DELETE_WINDOW in X11/Windows or a close event in macOS) to your application.

  1. Default Behavior: By default, many frameworks simply call a “hide” or “destroy” method on the main window. However, if your Python script has other non-daemon threads running or if the mainloop isn’t properly signaled to exit, the Python interpreter remains active.
  2. The Event Loop: Python GUI frameworks rely on an event loop (root.mainloop() or app.exec()). If you have a blocking while loop outside of this event loop or running in a way that ignores the “destroy” signal, the process won’t terminate.
  3. Resource Cleanup: If your app is connected to a database or a camera, simply “closing” the window doesn’t close those connections. You need a hook to perform cleanup.

Solution 2: Handling Closures in PyQt/PySide

Section titled “Solution 2: Handling Closures in PyQt/PySide”

If you are using PyQt6 or PySide6, the approach is slightly different. You must override the closeEvent method of your main window class.

# Python 3.11+, PyQt6 / PySide6 (illustrative example)
import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QMessageBox
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("PyQt Close Handler")
def closeEvent(self, event):
# This method is called automatically when the X button is clicked
reply = QMessageBox.question(self, 'Window Close', 'Are you sure you want to close?',
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.Yes:
# Perform cleanup here (e.g., close files, stop threads)
event.accept()
else:
event.ignore()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

If you have background threads (using the threading module), the Python process will stay alive until all non-daemon threads finish.

  • The Fix: Set thread.daemon = True when creating threads. This tells Python that the thread can be killed immediately when the main program exits.

2. The “Quit” vs. “Destroy” Confusion

Section titled “2. The “Quit” vs. “Destroy” Confusion”

In Tkinter, developers often confuse root.quit() and root.destroy().

  • root.quit() stops the mainloop(), but the widgets are still in memory and the script continues execution from the line after root.mainloop().
  • root.destroy() kills all widgets and usually ends the process if no other code follows.

If you run a heavy computation on the main thread (the same thread as the GUI), the “X” button will appear frozen and won’t respond to clicks.

  • The Fix: Move heavy tasks to a separate thread or use root.after() (Tkinter) or QTimer (PyQt) to break the task into small chunks.

How do I prevent the user from closing the app during a critical save? You can use the solutions above but conditionally call event.ignore() (PyQt) or simply return from the function without calling root.destroy() (Tkinter) if a “saving” flag is set to true.

Does this work differently on macOS? Yes. On macOS, clicking the red “X” often only closes the window but keeps the application in the Dock. If you want the app to quit entirely on macOS when the last window is closed, you may need to implement specific application-level delegates, though root.destroy() in Tkinter generally forces a full exit across all platforms.

What if my app has multiple windows? If you have a “Main” window and several “TopLevel” windows, make sure the WM_DELETE_WINDOW protocol is specifically handled on the root window to ensure the entire application exits when the main interface is closed.