Clean up on exit — Stopping threads when closing a PyQt6 application

How to properly shut down background threads and workers when your application window is closed
Heads up! You've already completed this tutorial.

I'm using QThreadPool and worker threads in my PyQt application. When I click the X button to close the window, the threads keep running in the background. What's the best way to clean everything up on application exit?

This is a very common situation when working with multithreaded PyQt6 applications. You've set up background workers using QThreadPool or QThread, everything runs great — but when you close the window, the application doesn't fully exit. The threads keep going, and you might even see errors in your console.

The solution is to hook into your window's close event and explicitly stop your background work before the window finishes closing. Let's walk through how to do this.

Understanding the problem

When you close a PyQt6 window by clicking the X button (or calling close()), Qt destroys the window and its widgets. But any threads you've started — whether via QThreadPool, QThread, or QRunnable — are managed separately. They don't automatically stop just because the window is gone.

This means your Python process can hang, or you might see tracebacks as threads try to interact with widgets that no longer exist.

Overriding closeEvent

Every QWidget (including QMainWindow) has a method called closeEvent that Qt calls whenever the widget is about to close. By overriding this method, you can run your own cleanup code at exactly the right moment.

Here's a minimal example:

python
from PyQt6.QtWidgets import QMainWindow


class MainWindow(QMainWindow):
    def closeEvent(self, event):
        # Put your cleanup code here
        print("Window is closing — cleaning up!")
        event.accept()

The event parameter is a QCloseEvent. Calling event.accept() tells Qt to go ahead and close the window. If you wanted to cancel the close (for example, to show a "Save changes?" dialog), you would call event.ignore() instead.

Stopping workers on close

If you're managing background workers — for example, through a QThreadPool — you'll want to signal them to stop and then wait for them to finish before allowing the window to close.

Here's how that looks in practice. First, let's set up a simple worker using QRunnable:

python
import time

from PyQt6.QtCore import QRunnable, pyqtSlot, QObject, pyqtSignal


class WorkerSignals(QObject):
    finished = pyqtSignal()


class Worker(QRunnable):
    def __init__(self):
        super().__init__()
        self.signals = WorkerSignals()
        self.is_running = True

    @pyqtSlot()
    def run(self):
        while self.is_running:
            print("Worker is working...")
            time.sleep(1)
        print("Worker stopped.")
        self.signals.finished.emit()

    def stop(self):
        self.is_running = False

The worker runs in a loop, checking self.is_running on each iteration. When stop() is called, it sets the flag to False, and the loop exits on the next check.

Now let's put together a QMainWindow that starts a worker and cleans it up on close:

python
import sys
import time

from PyQt6.QtCore import QRunnable, QThreadPool, pyqtSlot, QObject, pyqtSignal
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel


class WorkerSignals(QObject):
    finished = pyqtSignal()


class Worker(QRunnable):
    def __init__(self):
        super().__init__()
        self.signals = WorkerSignals()
        self.is_running = True

    @pyqtSlot()
    def run(self):
        while self.is_running:
            print("Worker is working...")
            time.sleep(1)
        print("Worker stopped.")
        self.signals.finished.emit()

    def stop(self):
        self.is_running = False


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Thread Cleanup Example")

        label = QLabel("Close this window to stop the worker.")
        self.setCentralWidget(label)

        self.threadpool = QThreadPool()
        self.workers = []

        # Start a background worker.
        worker = Worker()
        self.workers.append(worker)
        self.threadpool.start(worker)

    def closeEvent(self, event):
        # Signal all workers to stop.
        for worker in self.workers:
            worker.stop()

        # Wait for all threads in the pool to finish.
        self.threadpool.waitForDone()
        print("All workers stopped. Closing application.")
        event.accept()


app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

When you run this and close the window, you'll see the worker print its "stopped" message, and the application exits cleanly.

Let's look at what's happening in closeEvent:

First, we loop through all our tracked workers and call stop() on each one, setting the flag that tells them to finish. Then we call self.threadpool.waitForDone(), which blocks until every runnable in the pool has completed. This ensures we don't pull the rug out from under a running thread. Finally, we call event.accept() to let the window close.

Managing multiple worker groups

If your application has different categories of workers — say, one group handling camera feeds and another running inference engines — you can keep separate lists and stop them all in closeEvent:

python
def closeEvent(self, event):
    for worker in self.feed_workers:
        worker.stop()
    for worker in self.engine_workers:
        worker.stop()

    self.threadpool.waitForDone()
    event.accept()

The same principle applies: signal every worker to stop, then wait for the thread pool to drain.

Ensuring a clean exit with sys.exit

You might notice that even after the window closes, the Python process occasionally doesn't exit cleanly. This usually happens when sys.exit() isn't receiving the application's return code properly.

The standard way to launch and exit a PyQt6 application is:

python
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

app.exec() starts the Qt event loop and returns an exit code (an integer) when the loop ends. Passing that code to sys.exit() ensures Python terminates with the correct status. In PyQt6, the method is app.exec() (in PyQt5, it was app.exec_()).

If you skip sys.exit() and just call app.exec(), Python may not tear down all its resources properly — particularly if threads or other objects are still being referenced. Wrapping it in sys.exit() triggers a proper SystemExit exception, which gives Python the chance to clean everything up.

Complete working example

Here's the full example all in one place, ready to copy and run:

python
import sys
import time

from PyQt6.QtCore import QRunnable, QThreadPool, pyqtSlot, QObject, pyqtSignal
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel


class WorkerSignals(QObject):
    finished = pyqtSignal()


class Worker(QRunnable):
    def __init__(self, worker_id):
        super().__init__()
        self.worker_id = worker_id
        self.signals = WorkerSignals()
        self.is_running = True

    @pyqtSlot()
    def run(self):
        while self.is_running:
            print(f"Worker {self.worker_id} is working...")
            time.sleep(1)
        print(f"Worker {self.worker_id} stopped.")
        self.signals.finished.emit()

    def stop(self):
        self.is_running = False


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Thread Cleanup on Exit")
        self.resize(400, 200)

        label = QLabel("Close this window to stop all workers.")
        label.setMargin(20)
        self.setCentralWidget(label)

        self.threadpool = QThreadPool()
        self.workers = []

        # Start a few background workers.
        for i in range(3):
            worker = Worker(worker_id=i)
            self.workers.append(worker)
            self.threadpool.start(worker)

    def closeEvent(self, event):
        print("Close event received. Stopping workers...")

        for worker in self.workers:
            worker.stop()

        self.threadpool.waitForDone()
        print("All workers stopped. Goodbye!")
        event.accept()


app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

When you run this, you'll see three workers printing messages to the console. Close the window, and you'll see each worker confirm it has stopped before the application exits.

Summary

  • Override closeEvent on your main window to run cleanup code when the user closes the application.
  • Set a flag on each worker to signal it should stop, and check that flag in the worker's loop.
  • Use QThreadPool.waitForDone() to block until all workers have finished before allowing the window to close.
  • Always wrap app.exec() with sys.exit() to ensure a clean process exit.

This pattern works well for any PyQt6 application with background threads, whether you're processing data, handling network requests, or running live camera feeds. Once you have it in place, your application will shut down gracefully every time. For a complete introduction to using QThreadPool and QRunnable for multithreading in PyQt6, see our Multithreading PyQt6 applications with QThreadPool tutorial. You may also find our guides on signals and slots and creating your first PyQt6 window helpful as you build out your application.

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

Packaging Python Applications with PyInstaller by Martin Fitzpatrick

This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.

More info Get the book

Martin Fitzpatrick

Clean up on exit — Stopping threads when closing a PyQt6 application was written by Martin Fitzpatrick.

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt. Martin founded PythonGUIs to provide easy to follow GUI programming tutorials to the Python community. He has written a number of popular Python books on the subject.