How to start/stop or pause running threads?

Control QRunnable workers in PyQt6 with start, stop, and pause functionality
Heads up! You've already completed this tutorial.

When you're running long-running tasks in a background thread using PyQt6's QThreadPool and QRunnable, you'll quickly run into a common question: how do I stop or pause a thread that's already running?

Unlike QThread, the QRunnable class doesn't come with built-in mechanisms for stopping or pausing. There's no .stop() method or .pause() signal you can call. But with a small amount of extra code, you can add this control yourself using simple Python flags that your worker checks periodically.

In this tutorial, we'll walk through how to build a threaded worker that you can start, stop, and pause/resume using buttons in your PyQt6 GUI.

The problem: QRunnable has no built-in stop or pause

If you've followed the Multithreading PyQt6 applications with QThreadPool tutorial, you'll know how to run background tasks using a QRunnable worker and QThreadPool. But once a worker is running, you don't have direct control over it — the thread pool manages execution for you.

So if your worker is doing something in a loop (processing data, polling a device, running a simulation), how do you tell it to stop? Or pause?

The answer is straightforward: use a shared flag — a simple boolean variable — that your worker checks on each iteration of its loop. When you want the worker to stop, you set the flag from your main thread, and the worker sees it and exits gracefully.

Creating a basic QRunnable worker with a loop

Let's start with a simple QRunnable worker that counts in a loop, emitting progress as it goes. This is the kind of long-running task you might want to control.

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


class WorkerSignals(QObject):
    """Signals available from a running worker thread."""

    progress = pyqtSignal(int)
    finished = pyqtSignal()


class Worker(QRunnable):
    """Worker thread for running background tasks."""

    def __init__(self):
        super().__init__()
        self.signals = WorkerSignals()

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

        self.signals.finished.emit()

This worker counts from 1 to 100, pausing briefly between each step. It emits a progress signal so the GUI can update, and a finished signal when it's done.

The problem? Once you start it, there's no way to stop it early or pause it mid-way. Let's fix that.

Adding stop and pause controls to QRunnable

To control the worker, we add two boolean attributes: is_paused and is_stopped. The worker checks these flags on every iteration of its loop.

Bring Your PyQt/PySide Application to Market — Specialized launch support for scientific and engineering software built using Python & Qt.

Find out More

python
class Worker(QRunnable):
    """Worker thread with stop and pause controls."""

    def __init__(self):
        super().__init__()
        self.signals = WorkerSignals()
        self.is_paused = False
        self.is_stopped = False

    @pyqtSlot()
    def run(self):
        for n in range(100):
            # Check if we've been told to stop.
            if self.is_stopped:
                break

            # If paused, wait here until resumed (or stopped).
            while self.is_paused:
                time.sleep(0.1)
                if self.is_stopped:
                    break

            time.sleep(0.1)
            self.signals.progress.emit(n + 1)

        self.signals.finished.emit()

    def stop(self):
        """Request the worker to stop."""
        self.is_stopped = True

    def pause(self):
        """Pause the worker."""
        self.is_paused = True

    def resume(self):
        """Resume the worker."""
        self.is_paused = False

Let's walk through what's happening here:

  • self.is_stopped: When set to True, the worker breaks out of its main loop on the next iteration. The finished signal is still emitted so the GUI knows the worker is done.

  • self.is_paused: When set to True, the worker enters a tight inner while loop that just sleeps. It stays there until is_paused is set back to False (via the resume() method). We also check is_stopped inside the pause loop, so you can stop a paused worker without needing to resume it first.

  • stop(), pause(), resume(): These are convenience methods that set the flags. You call them from your main thread (e.g., from button click handlers).

The time.sleep(0.1) inside the pause loop keeps the thread from spinning at full speed while it waits. Without it, the paused thread would consume CPU unnecessarily.

Complete example: PyQt6 GUI with start, stop, and pause buttons

Now let's build a small PyQt6 window with buttons to Start, Pause/Resume, and Stop the worker, plus a progress bar to see what's happening.

python
import sys
import time

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


class WorkerSignals(QObject):
    """Signals available from a running worker thread."""

    progress = pyqtSignal(int)
    finished = pyqtSignal()


class Worker(QRunnable):
    """Worker thread with stop and pause controls."""

    def __init__(self):
        super().__init__()
        self.signals = WorkerSignals()
        self.is_paused = False
        self.is_stopped = False

    @pyqtSlot()
    def run(self):
        for n in range(100):
            if self.is_stopped:
                break

            while self.is_paused:
                time.sleep(0.1)
                if self.is_stopped:
                    break

            time.sleep(0.1)
            self.signals.progress.emit(n + 1)

        self.signals.finished.emit()

    def stop(self):
        """Request the worker to stop."""
        self.is_stopped = True

    def pause(self):
        """Pause the worker."""
        self.is_paused = True

    def resume(self):
        """Resume the worker."""
        self.is_paused = False


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

        self.threadpool = QThreadPool()
        self.worker = None

        # Create widgets.
        self.progress_bar = QProgressBar()
        self.progress_bar.setRange(0, 100)

        self.start_button = QPushButton("Start")
        self.pause_button = QPushButton("Pause")
        self.stop_button = QPushButton("Stop")

        # Disable pause and stop until a worker is running.
        self.pause_button.setEnabled(False)
        self.stop_button.setEnabled(False)

        # Connect buttons.
        self.start_button.clicked.connect(self.start_worker)
        self.pause_button.clicked.connect(self.pause_worker)
        self.stop_button.clicked.connect(self.stop_worker)

        # Layout.
        button_layout = QHBoxLayout()
        button_layout.addWidget(self.start_button)
        button_layout.addWidget(self.pause_button)
        button_layout.addWidget(self.stop_button)

        layout = QVBoxLayout()
        layout.addWidget(self.progress_bar)
        layout.addLayout(button_layout)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

    def start_worker(self):
        """Start a new worker thread."""
        self.worker = Worker()
        self.worker.signals.progress.connect(self.update_progress)
        self.worker.signals.finished.connect(self.worker_finished)

        # Update button states.
        self.start_button.setEnabled(False)
        self.pause_button.setEnabled(True)
        self.stop_button.setEnabled(True)
        self.pause_button.setText("Pause")

        # Reset progress bar.
        self.progress_bar.setValue(0)

        self.threadpool.start(self.worker)

    def pause_worker(self):
        """Toggle pause/resume on the worker."""
        if self.worker is None:
            return

        if self.worker.is_paused:
            self.worker.resume()
            self.pause_button.setText("Pause")
        else:
            self.worker.pause()
            self.pause_button.setText("Resume")

    def stop_worker(self):
        """Stop the running worker."""
        if self.worker is None:
            return

        self.worker.stop()

    def update_progress(self, value):
        """Update the progress bar."""
        self.progress_bar.setValue(value)

    def worker_finished(self):
        """Handle worker completion."""
        self.start_button.setEnabled(True)
        self.pause_button.setEnabled(False)
        self.stop_button.setEnabled(False)
        self.pause_button.setText("Pause")
        self.worker = None


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

Run this and you'll see a window with a progress bar and three buttons. Click Start and the progress bar begins filling. Click Pause and it freezes in place — click it again (now labeled Resume) and it continues. Click Stop and the worker exits early.

Thread control example with start, pause and stop buttons

How the start, stop, and pause signals work

Let's trace through what happens when you interact with the buttons:

Starting: start_worker() creates a new Worker instance, connects its signals, and hands it to the thread pool. The pool assigns it to an available thread and calls run().

Pausing: pause_worker() calls self.worker.pause(), which sets is_paused = True. On the next loop iteration, the worker enters the inner while self.is_paused loop and just sleeps. The progress bar stops updating. When you click the button again, resume() sets is_paused = False, and the worker continues its main loop from where it left off.

Stopping: stop_worker() calls self.worker.stop(), which sets is_stopped = True. On the next iteration (or inside the pause loop), the worker sees this flag and breaks out. The finished signal fires, and worker_finished() resets the GUI.

Important considerations for threaded PyQt6 workers

Flags are checked between iterations. The worker only checks is_stopped and is_paused at the top of each loop iteration. If a single iteration takes a long time (e.g., a heavy computation or a blocking network call), there will be a delay before the worker responds to your stop or pause request. This is a cooperative approach — the worker has to agree to stop.

Auto-delete behavior. By default, QRunnable objects are auto-deleted by the thread pool after run() completes. If you need to keep a reference to the worker after it finishes (to read results, for example), call self.setAutoDelete(False) in the worker's __init__. In our example, we clear the reference in worker_finished(), so auto-delete is fine.

Thread safety. Setting a simple boolean from one thread and reading it from another works reliably in practice with CPython, thanks to the GIL (Global Interpreter Lock). For more complex shared state, you'd want to use proper synchronization primitives like QMutex. But for boolean flags like these, you're safe.

Don't force-kill threads. You might be tempted to look for a way to forcefully terminate a thread. Resist that temptation. Force-killing threads can leave your application in an inconsistent state — locks held, resources leaked, signals half-emitted. The cooperative flag-based approach shown here is the right way to do it.

Adapting this pattern for your own PyQt6 application

To adapt this pattern for your own application, replace the for loop and time.sleep() in the worker's run() method with your actual task. The pattern works well for any loop-based workload:

  • Processing items from a list or queue
  • Polling an external device or API
  • Running a simulation step by step
  • Reading or writing data in chunks

Just make sure to check is_stopped and is_paused at a reasonable frequency within your loop. If your loop body does something that takes a few seconds, consider adding the check at multiple points within the loop body.

python
@pyqtSlot()
def run(self):
    for item in self.data:
        if self.is_stopped:
            break

        while self.is_paused:
            time.sleep(0.1)
            if self.is_stopped:
                break

        # Do your actual work here.
        result = process(item)
        self.signals.progress.emit(result)

    self.signals.finished.emit()

That's all there is to it. With a couple of boolean flags and some cooperative checking, you get full start/stop/pause control over your background workers — and your PyQt6 GUI stays responsive the whole time.

1:1 Coaching & Tutoring for your Python GUIs project
Martin Fitzpatrick Python GUIs Coaching & Training
60 mins ($195) Book Now

1:1 Python GUIs Coaching & Training

Comprehensive code reviewBugfixes & improvements • Maintainability advice and architecture improvements • Design and usability assessment • Suggestions and tips to expand your knowledgePackaging and distribution help for Windows, Mac & Linux • Find out more.

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

How to start/stop or pause running threads? 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.