Pause a running worker thread

Put a running task on hold, waiting for the UI
Heads up! You've already completed this tutorial.

I have a worker running in a QThreadPool. How can I pause it until the user interacts with the GUI, and then resume it? Also, how do I gracefully stop the worker when the application closes?

When you're running a long task in a background thread using QThreadPool and QRunnable, you'll sometimes need to pause that task — maybe to wait for user input, or to let them review something before continuing. You might also want to stop the task entirely, either from a button or when the user closes the window.

In this article, you'll build a small PyQt6 application with a progress bar and three buttons: Pause, Resume, and Stop. You'll learn how to control a running worker from the main thread, and how to shut everything down cleanly when the application exits.

Remember, that QThreadPool has a limited number of slots for running QRunnable threads. Pausing doesn't release the thread to be used by another QRunnable, so there will be fewer threads available to do work, while the QRunnable is paused.

The approach

The worker runs in a loop, doing its work step by step. To make it pausable, you add a flag (is_paused) that the loop checks on each iteration. When the flag is True, the worker enters a tight waiting loop — sleeping briefly and checking again — until the flag is cleared.

To stop the worker, you use a second flag (is_killed). When this is set, the worker exits the loop and finishes.

Both flags are simple Python booleans set via methods on the worker object. From the main thread, you call these methods directly — for example, by connecting them to button signals.

This works safely because you're only ever setting a boolean from one thread and reading it from another. Python's Global Interpreter Lock (GIL) ensures that reading and writing simple attributes like this won't cause data corruption.

Building the worker

Start by defining the signals your worker will emit, and the worker class itself:

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

class WorkerSignals(QObject):
    progress = pyqtSignal(int)

class JobRunner(QRunnable):

    signals = WorkerSignals()

    def __init__(self):
        super().__init__()
        self.is_paused = False
        self.is_killed = False

    @pyqtSlot()
    def run(self):
        for n in range(100):
            self.signals.progress.emit(n + 1)
            time.sleep(0.1)

            while self.is_paused:
                time.sleep(0)

            if self.is_killed:
                break

    def pause(self):
        self.is_paused = True

    def resume(self):
        self.is_paused = False

    def kill(self):
        self.is_killed = True

The run method is where the actual work happens. On each iteration of the loop it:

  1. Emits a progress signal with the current step number.
  2. Sleeps briefly (simulating real work).
  3. Checks the is_paused flag — if True, it enters a waiting loop.
  4. Checks the is_killed flag — if True, it breaks out of the loop entirely.

The pause(), resume(), and kill() methods are plain Python methods that update the flags. You'll connect these to button signals from the main window.

The time.sleep(0) inside the pause loop is worth a quick note. Calling sleep(0) yields control to other threads without introducing a noticeable delay. This keeps the worker responsive to flag changes without burning CPU cycles in a busy loop.

Building the main window

Now set up a window with the three control buttons and a progress bar:

python
from PyQt6.QtWidgets import (
    QApplication,
    QMainWindow,
    QWidget,
    QHBoxLayout,
    QPushButton,
    QProgressBar,
)
from PyQt6.QtCore import QThreadPool

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        w = QWidget()
        layout = QHBoxLayout()
        w.setLayout(layout)

        btn_stop = QPushButton("Stop")
        btn_pause = QPushButton("Pause")
        btn_resume = QPushButton("Resume")

        layout.addWidget(btn_stop)
        layout.addWidget(btn_pause)
        layout.addWidget(btn_resume)

        self.setCentralWidget(w)

        self.status = self.statusBar()
        self.progress = QProgressBar()
        self.status.addPermanentWidget(self.progress)

        # Set up the thread pool and runner.
        self.threadpool = QThreadPool()
        self.runner = JobRunner()
        self.runner.signals.progress.connect(self.update_progress)
        self.threadpool.start(self.runner)

        # Connect buttons to worker methods.
        btn_stop.pressed.connect(self.runner.kill)
        btn_pause.pressed.connect(self.runner.pause)
        btn_resume.pressed.connect(self.runner.resume)

        self.show()

    def update_progress(self, n):
        self.progress.setValue(n)

The runner is created, its progress signal is connected to update_progress, and it's started on the thread pool. Each button is connected directly to the corresponding method on the runner.

When you press Pause, the pause() method sets is_paused = True, and the worker's loop starts waiting. Press Resume and is_paused becomes False again, so the loop continues. Press Stop and the loop exits completely.

Graceful shutdown on close

There's one more thing to handle: if the user closes the window while the worker is still running, the thread will keep going in the background. You may see errors in the console, or the process may hang.

To fix this, connect the application's aboutToQuit signal to a shutdown method that stops the worker. Since the runner is created inside the MainWindow, you need a method on the window that the application can call:

python
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.runner = None  # Defined early so shutdown() is always safe.
        # ... rest of __init__ ...

    def shutdown(self):
        if self.runner:
            self.runner.kill()

Then, after creating the window, connect the signal:

python
app.aboutToQuit.connect(w.shutdown)

Setting self.runner = None at the top of __init__ (before the runner is actually created) means shutdown() won't crash if the window is closed before the runner has been set up.

If you have multiple workers, you could keep them in a list and loop through them in shutdown(), calling kill() on each one.

Complete working example

Here's everything together in a single file you can copy and run:

python
import sys
import time

from PyQt6.QtCore import (
    QObject,
    QRunnable,
    QThreadPool,
    pyqtSignal,
    pyqtSlot
)
from PyQt6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QMainWindow,
    QProgressBar,
    QPushButton,
    QWidget,
)

class WorkerSignals(QObject):
    progress = pyqtSignal(int)

class JobRunner(QRunnable):

    signals = WorkerSignals()

    def __init__(self):
        super().__init__()
        self.is_paused = False
        self.is_killed = False

    @pyqtSlot()
    def run(self):
        for n in range(100):
            self.signals.progress.emit(n + 1)
            time.sleep(0.1)

            while self.is_paused:
                time.sleep(0)

            if self.is_killed:
                break

    def pause(self):
        self.is_paused = True

    def resume(self):
        self.is_paused = False

    def kill(self):
        self.is_killed = True

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.runner = None

        w = QWidget()
        layout = QHBoxLayout()
        w.setLayout(layout)

        btn_stop = QPushButton("Stop")
        btn_pause = QPushButton("Pause")
        btn_resume = QPushButton("Resume")

        layout.addWidget(btn_stop)
        layout.addWidget(btn_pause)
        layout.addWidget(btn_resume)

        self.setCentralWidget(w)

        self.status = self.statusBar()
        self.progress = QProgressBar()
        self.status.addPermanentWidget(self.progress)

        # Set up the thread pool and runner.
        self.threadpool = QThreadPool()
        self.runner = JobRunner()
        self.runner.signals.progress.connect(self.update_progress)
        self.threadpool.start(self.runner)

        # Connect buttons to worker methods.
        btn_stop.pressed.connect(self.runner.kill)
        btn_pause.pressed.connect(self.runner.pause)
        btn_resume.pressed.connect(self.runner.resume)

        self.show()

    def update_progress(self, n):
        self.progress.setValue(n)

    def shutdown(self):
        if self.runner:
            self.runner.kill()

app = QApplication(sys.argv)
w = MainWindow()
app.aboutToQuit.connect(w.shutdown)
app.exec()

When you run this, a window appears with a progress bar filling up from left to right. Press Pause to freeze it in place, Resume to continue, or Stop to end the task. If you close the window at any point, the aboutToQuit signal fires and the worker is stopped cleanly.

Progress bar with pause, resume and stop buttons

Waiting for user input

The same pattern works if you want to pause the worker until the user types something or makes a selection in the GUI. Instead of connecting the resume() method to a button, you could connect it to any signal — for example, a QLineEdit.returnPressed signal or a dialog's accepted signal. The worker will sit in its waiting loop until you call resume(), regardless of what triggers that call.

This gives you a flexible way to coordinate between the background thread and the user interface without needing to stop and restart workers or manage complex signal-slot wiring between threads.

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

Create GUI Applications with Python & Qt6 by Martin Fitzpatrick

(PyQt6 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!

More info Get the book

Martin Fitzpatrick

Pause a running worker thread was written by Martin Fitzpatrick with contributions from Leo Well.

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.