I'm using QThreadPool for multithreading in my PyQt application. What happens if the user closes the GUI while one or more threads are still running in the pool? It throws an exception. My idea is to signal from
closeEvent, wait for a few seconds while the task finishes, and then quit. Is there a better way to handle this?
When you're running background tasks using QThreadPool and the user closes the application window, things can go wrong. The threads may still be running, trying to emit signals back to widgets that no longer exist. This often results in errors or crashes on shutdown.
The good news is that you can handle this cleanly. In this article, we'll walk through how to gracefully shut down running threads when your PyQt6 application closes.
Why does closing cause problems?
When you close a PyQt6 window, the window and its widgets are destroyed. But if a thread in your QThreadPool is still running, it may try to emit a signal to a slot on one of those destroyed widgets. Qt doesn't like this, and you'll typically see a runtime error or a crash.
The root of the problem: the threads don't know the application is shutting down, so they keep working as if nothing has changed.
The approach: tell your workers to stop
The cleanest way to handle this is to give your worker a way to check whether it should stop running. You do this by setting a flag on the worker, and having the worker check that flag periodically during its work. When the application is closing, you set the flag, and the worker wraps up on its own.
Let's build this up step by step.
A basic worker with a stop flag
Here's a Worker class that extends QRunnable. It has a boolean attribute is_killed that starts as False. Inside the run() method, the worker checks this flag on each iteration of its loop. If is_killed becomes True, the worker exits early.
from PyQt6.QtCore import QRunnable, pyqtSlot, pyqtSignal, QObject
class WorkerSignals(QObject):
progress = pyqtSignal(int)
finished = pyqtSignal()
class Worker(QRunnable):
def __init__(self):
super().__init__()
self.signals = WorkerSignals()
self.is_killed = False
@pyqtSlot()
def run(self):
for n in range(100):
if self.is_killed:
# Exit the loop early if we've been told to stop.
return
self.signals.progress.emit(n + 1)
# Simulate some work.
import time
time.sleep(0.1)
self.signals.finished.emit()
def kill(self):
self.is_killed = True
The kill() method is a simple way to set the flag from outside the worker. You could also set is_killed directly, but having a method keeps things tidy.
The check if self.is_killed inside the loop is what makes this work. Every time the worker goes around the loop, it looks at the flag. If it's been set to True, the worker returns immediately without doing any more work or emitting any more signals.
PyQt/PySide Development Services — Stuck in development hell? I'll help you get your project focused, finished and released. Benefit from years of practical experience releasing software with Python.
If you're new to using QThreadPool and QRunnable for multithreading, see our tutorial on Multithreading PyQt6 applications with QThreadPool for a full introduction.
Keeping track of your workers
To be able to stop workers at shutdown, you need to keep references to them. A simple list on your main window works well for this:
self.workers = []
Each time you create and start a worker, append it to the list:
worker = Worker()
self.threadpool.start(worker)
self.workers.append(worker)
Now your main window has a handle on every worker that's been started.
Stopping workers in closeEvent
Qt provides a closeEvent method on QWidget (and therefore on QMainWindow) that is called whenever the window is about to close. This is the perfect place to tell all your workers to stop and then wait for them to finish.
Here's what that looks like:
def closeEvent(self, event):
# Tell all workers to stop.
for worker in self.workers:
worker.kill()
# Wait for all threads to finish.
self.threadpool.waitForDone()
# Accept the close event so the window actually closes.
event.accept()
self.threadpool.waitForDone() blocks until all threads in the pool have completed. Because we've already set the kill flag on each worker, they will finish their current iteration and exit their run() method, which means waitForDone() won't have to wait long.
If you want to set a maximum wait time (in milliseconds) as a safety net, you can pass it as an argument:
self.threadpool.waitForDone(5000) # Wait up to 5 seconds.
This ensures the application doesn't hang indefinitely if something goes wrong in a worker.
Complete working example
Here's a full application that starts a background worker, displays its progress on a progress bar, and shuts down cleanly when the window is closed:
import sys
import time
from PyQt6.QtCore import QRunnable, QThreadPool, QObject, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import (
QApplication,
QMainWindow,
QProgressBar,
QPushButton,
QVBoxLayout,
QWidget,
)
class WorkerSignals(QObject):
progress = pyqtSignal(int)
finished = pyqtSignal()
class Worker(QRunnable):
def __init__(self):
super().__init__()
self.signals = WorkerSignals()
self.is_killed = False
@pyqtSlot()
def run(self):
for n in range(100):
if self.is_killed:
return
self.signals.progress.emit(n + 1)
time.sleep(0.1)
self.signals.finished.emit()
def kill(self):
self.is_killed = True
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Thread Shutdown Example")
self.threadpool = QThreadPool()
self.workers = []
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 100)
self.button = QPushButton("Start Task")
self.button.clicked.connect(self.start_worker)
layout = QVBoxLayout()
layout.addWidget(self.progress_bar)
layout.addWidget(self.button)
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
def start_worker(self):
worker = Worker()
worker.signals.progress.connect(self.progress_bar.setValue)
worker.signals.finished.connect(self.worker_finished)
self.threadpool.start(worker)
self.workers.append(worker)
def worker_finished(self):
self.progress_bar.setValue(100)
def closeEvent(self, event):
for worker in self.workers:
worker.kill()
self.threadpool.waitForDone()
event.accept()
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Run this, click Start Task, and then close the window while the progress bar is still moving. The application will shut down cleanly — no errors, no crashes.
A few things to keep in mind
Check the flag frequently. The is_killed check only works if the worker looks at it often enough. If your worker does one very long operation (like a single large network request), the flag won't help until that operation completes. In those cases, you might need to restructure the work into smaller chunks, or use a timeout on the blocking operation.
Clean up finished workers. In a long-running application, you might want to remove finished workers from the list to avoid it growing indefinitely. You can connect the finished signal to a method that removes the worker:
worker.signals.finished.connect(lambda w=worker: self.workers.remove(w))
Thread safety of the flag. In Python, setting a boolean attribute like self.is_killed = True is effectively atomic due to the GIL (Global Interpreter Lock), so you don't need a mutex or lock for this simple flag pattern. If you were coordinating more complex shared state between threads, you'd want to use QMutex or Python's threading.Lock.
waitForDone() blocks the main thread. While waiting, the UI will be unresponsive. For workers that exit quickly after being killed, this is usually fine. If your workers might take a while to finish even after being killed, consider showing a "shutting down..." message or using a timeout.
This pattern — a kill flag checked inside the worker's loop, combined with waitForDone() in closeEvent — gives you a clean and predictable shutdown for threaded PyQt6 applications. For more on how to transmit data between threads using signals, or how to pause and stop running workers, check out our other threading guides.
PyQt6 Crash Course by Martin Fitzpatrick — The important parts of PyQt6 in bite-size chunks