Updating a QProgressBar from a QRunnable in PyQt6

How to safely update GUI widgets from background threads using signals and slots
Heads up! You've already completed this tutorial.

I want to update a QProgressBar widget from a QRunnable background thread, but I'm getting the error: QObject::setParent: Cannot set parent, new parent is in a different thread. How do I safely update a progress bar from a worker thread? And what about cases where the long-running task is a single blocking call — how do I show smooth progress then?

If you've started using QThreadPool and QRunnable to run long tasks in the background, you've probably hit this wall: you want to update a progress bar (or other widgets) from your worker, but Qt won't let you touch GUI elements from any thread other than the main one.

The solution is straightforward — emit signals from your worker thread and connect them to slots on your main window. The signal carries a simple value (like an integer from 0 to 100), and the slot on the main thread uses that value to update the progress bar. Qt handles the cross-thread delivery for you.

Let's walk through how this works, and then look at a couple of common real-world patterns.

The basic pattern: emitting progress from a QRunnable

All GUI widgets in Qt must live on the main thread. You can't create, move, or modify widgets from a background thread. That's what causes the Cannot set parent, new parent is in a different thread error.

Instead, your background worker should only emit signals with data (like a progress value), and your main window should receive those signals and update the GUI.

Here's a complete working example in PyQt6:

python
import time

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


class WorkerSignals(QObject):
    """Signals available from a running worker thread."""
    progress = pyqtSignal(int)


class JobRunner(QRunnable):
    """A worker that emits progress signals."""

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

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


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

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

        self.threadpool = QThreadPool()

        self.runner = JobRunner()
        self.runner.signals.progress.connect(self.update_progress)
        self.threadpool.start(self.runner)

        self.show()

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


app = QApplication([])
w = MainWindow()
app.exec()

When you run this, a window appears with a progress bar in the status bar. It moves from 0 to 100, incrementing by 1 every tenth of a second. The JobRunner does the work in the background and sends int values via the progress signal. The main window receives those values in update_progress and sets them on the progress bar.

A progress bar in the window status bar, updating from a background thread

A few things to notice:

  • WorkerSignals is a separate QObject subclass. QRunnable doesn't inherit from QObject, so it can't have signals directly. We create a small helper class to hold them and create an instance of it on our runner.
  • The signal carries an int. This is the only data that crosses the thread boundary. No widgets, no GUI objects — just a number.
  • The connection is made before starting the runner. We connect self.runner.signals.progress to self.update_progress first, then call self.threadpool.start(self.runner).

If you're new to signals and slots, the Signals, Slots & Events tutorial explains how this mechanism works in detail.

The complete guide to packaging Python GUI applications with PyInstaller.

Handling a single blocking function call

The example above has a nice loop where we can emit progress at each step. But what if your long-running task is a single function call that takes 25–30 seconds and you can't modify it? You can't emit progress during that call because your code is blocked until it returns.

You have a few options here.

Use an indeterminate progress bar

If you genuinely can't report incremental progress, you can set the progress bar to "busy" mode. This shows an animated bar that bounces back and forth, telling the user something is happening without pretending to know how far along it is.

python
import time

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


class WorkerSignals(QObject):
    finished = pyqtSignal()


class JobRunner(QRunnable):

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

    @pyqtSlot()
    def run(self):
        # Simulate a single long blocking call.
        time.sleep(5)
        self.signals.finished.emit()


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        self.status = self.statusBar()
        self.progress = QProgressBar()
        # Setting min and max to 0 enables "busy" mode.
        self.progress.setMinimum(0)
        self.progress.setMaximum(0)
        self.status.addPermanentWidget(self.progress)

        self.threadpool = QThreadPool()

        self.runner = JobRunner()
        self.runner.signals.finished.connect(self.task_finished)
        self.threadpool.start(self.runner)

        self.show()

    def task_finished(self):
        self.progress.setMaximum(100)
        self.progress.setValue(100)
        self.status.showMessage("Done!")


app = QApplication([])
w = MainWindow()
app.exec()

Setting both minimum and maximum to 0 puts the progress bar into an indeterminate "pulsing" mode. When the task finishes, we reset the maximum to 100 and set the value to show completion.

Simulate smooth progress with a timer

If you'd rather show an estimated smooth progress bar (for example, going from 10% to 90% over a known duration), you can use a QTimer on the main thread to increment the bar while the background task runs.

python
import time

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


class WorkerSignals(QObject):
    finished = pyqtSignal()


class JobRunner(QRunnable):

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

    @pyqtSlot()
    def run(self):
        # Simulate a single long blocking call (~5 seconds).
        time.sleep(5)
        self.signals.finished.emit()


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

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

        self.threadpool = QThreadPool()

        # Set up a timer to simulate smooth progress.
        self.estimated_progress = 10
        self.progress.setValue(self.estimated_progress)

        self.timer = QTimer()
        self.timer.setInterval(200)  # Update every 200ms.
        self.timer.timeout.connect(self.increment_progress)

        # Start the background task.
        self.runner = JobRunner()
        self.runner.signals.finished.connect(self.task_finished)
        self.threadpool.start(self.runner)
        self.timer.start()

        self.show()

    def increment_progress(self):
        if self.estimated_progress < 90:
            self.estimated_progress += 1
            self.progress.setValue(self.estimated_progress)

    def task_finished(self):
        self.timer.stop()
        self.progress.setValue(100)
        self.status.showMessage("Done!")


app = QApplication([])
w = MainWindow()
app.exec()

The QTimer runs on the main thread and ticks every 200 milliseconds, nudging the progress bar forward. It caps at 90% so it doesn't hit 100% before the real work is done. When the finished signal arrives from the worker, we stop the timer and jump to 100%.

This gives a smooth visual experience even when you can't get real progress data from the blocking function.

Multi-step tasks: updating widgets between stages

Another common pattern is having several steps in a background process, where you need to update labels, enable buttons, or change icons between each step. Since all GUI updates must happen on the main thread, you have two good approaches.

Emit status signals with step information

Rather than splitting your task into separate QRunnable objects, you can emit a signal carrying the step number or a status string. The main window slot then updates whatever widgets it needs based on that information.

python
import time

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


class WorkerSignals(QObject):
    progress = pyqtSignal(int)
    status = pyqtSignal(str)
    finished = pyqtSignal()


class MultiStepRunner(QRunnable):

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

    @pyqtSlot()
    def run(self):
        self.signals.status.emit("Running step 1: Downloading data...")
        self.signals.progress.emit(0)
        time.sleep(2)  # Simulate step 1.

        self.signals.status.emit("Running step 2: Processing data...")
        self.signals.progress.emit(33)
        time.sleep(2)  # Simulate step 2.

        self.signals.status.emit("Running step 3: Saving results...")
        self.signals.progress.emit(66)
        time.sleep(2)  # Simulate step 3.

        self.signals.progress.emit(100)
        self.signals.status.emit("All steps complete!")
        self.signals.finished.emit()


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        layout = QVBoxLayout()

        self.status_label = QLabel("Ready")
        layout.addWidget(self.status_label)

        self.progress = QProgressBar()
        layout.addWidget(self.progress)

        self.start_button = QPushButton("Start")
        self.start_button.clicked.connect(self.start_task)
        layout.addWidget(self.start_button)

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

        self.threadpool = QThreadPool()

        self.show()

    def start_task(self):
        self.start_button.setEnabled(False)

        runner = MultiStepRunner()
        runner.signals.progress.connect(self.update_progress)
        runner.signals.status.connect(self.update_status)
        runner.signals.finished.connect(self.task_finished)
        self.threadpool.start(runner)

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

    def update_status(self, text):
        self.status_label.setText(text)

    def task_finished(self):
        self.start_button.setEnabled(True)


app = QApplication([])
w = MainWindow()
app.exec()

Here, MultiStepRunner emits both progress (an int) and status (a str) signals at each stage. The main window connects to both and updates the progress bar and label accordingly. The button is disabled while the task runs and re-enabled when it finishes.

This is simpler and cleaner than chaining separate QRunnable objects together. All your task logic stays in one place, and the main window just reacts to whatever signals arrive.

Chaining separate runners

If your steps are genuinely independent (perhaps they're reusable operations used elsewhere in your app), you can chain them by connecting the finished signal of one runner to a method that starts the next. This works fine, but it adds complexity. For most cases, the single runner with status signals shown above is easier to maintain.

If you do need to pass more complex data between threads, the Transmitting extra data with Qt signals tutorial covers that in detail.

Complete working example

Here's a final, combined example that brings together a progress bar, status label, and a multi-step background task:

python
import time

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


class WorkerSignals(QObject):
    """Defines the signals available from a running worker thread."""
    progress = pyqtSignal(int)
    status = pyqtSignal(str)
    finished = pyqtSignal()


class MultiStepRunner(QRunnable):
    """A worker that runs multiple steps and reports progress."""

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

    @pyqtSlot()
    def run(self):
        steps = [
            ("Downloading data...", 2),
            ("Processing data...", 3),
            ("Saving results...", 1),
        ]

        total_steps = len(steps)

        for i, (description, duration) in enumerate(steps):
            self.signals.status.emit(f"Step {i + 1}/{total_steps}: {description}")
            self.signals.progress.emit(int((i / total_steps) * 100))
            time.sleep(duration)  # Simulate work.

        self.signals.progress.emit(100)
        self.signals.status.emit("All steps complete!")
        self.signals.finished.emit()


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        self.setWindowTitle("Progress Bar with QRunnable")

        layout = QVBoxLayout()

        self.status_label = QLabel("Press Start to begin.")
        layout.addWidget(self.status_label)

        self.progress = QProgressBar()
        layout.addWidget(self.progress)

        self.start_button = QPushButton("Start")
        self.start_button.clicked.connect(self.start_task)
        layout.addWidget(self.start_button)

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

        self.threadpool = QThreadPool()

        self.show()

    def start_task(self):
        self.start_button.setEnabled(False)
        self.progress.setValue(0)

        runner = MultiStepRunner()
        runner.signals.progress.connect(self.update_progress)
        runner.signals.status.connect(self.update_status)
        runner.signals.finished.connect(self.task_finished)
        self.threadpool.start(runner)

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

    def update_status(self, text):
        self.status_label.setText(text)

    def task_finished(self):
        self.start_button.setEnabled(True)


app = QApplication([])
w = MainWindow()
app.exec()

Run this and click Start. You'll see the label update with each step's description, the progress bar advance, and the button re-enable when everything is done. All the heavy work happens in the background, and the GUI stays responsive throughout.

For a deeper dive into multithreading with Qt, see the full Multithreading with QThreadPool tutorial.

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

Find out More

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

Updating a QProgressBar from a QRunnable in PyQt6 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.