Understanding Python Keyword Unpacking with QThreadPool Progress Callbacks in PyQt6

How **kwargs connects progress signals to worker functions in PyQt6 multithreading
Heads up! You've already completed this tutorial.

How does adding progress_callback to self.kwargs in a QThreadPool worker connect it to the execute_this_fn(self, progress_callback) function? How does the signal end up as a parameter in the function?

If you've been working through PyQt6's QThreadPool multithreading examples, you may have come across a pattern where a progress_callback signal is added to a dictionary called self.kwargs, and then somehow ends up as a named parameter inside a worker function. The connection between these two things relies on a Python feature called keyword unpacking, and once you see how it works, the whole pattern makes a lot more sense.

What the code looks like

Here's the pattern in question. Inside a Worker class that runs tasks on a QThreadPool, you'll often see something like this:

python
# Add the callback to our kwargs
self.kwargs['progress_callback'] = self.signals.progress

And then a function that receives it:

python
def execute_this_fn(progress_callback):
    for n in range(0, 5):
        time.sleep(1)
        progress_callback.emit(n * 100 / 4)

The question is: how does the value stored in self.kwargs['progress_callback'] end up as the progress_callback parameter in execute_this_fn? The answer is in how the function gets called.

In the Worker.run() method, the function is called like this:

python
result = self.fn(**self.kwargs)

That ** in front of self.kwargs is doing all the work, unpacking the keywords and passing them to the function.

Python keyword unpacking explained

In Python, you can pass keyword arguments to a function using a dictionary. When you place ** before a dictionary in a function call, Python takes each key-value pair in the dictionary and passes them as keyword arguments.

Here's a simple example to illustrate:

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.

Find out More

python
def greet(name, greeting):
    print(f"{greeting}, {name}!")

my_args = {
    "name": "Alice",
    "greeting": "Hello",
}

greet(**my_args)

This outputs:

python
Hello, Alice!

When Python sees greet(**my_args), it unpacks the dictionary so the call becomes equivalent to:

python
greet(name="Alice", greeting="Hello")

Each key in the dictionary matches a parameter name in the function, and the corresponding value is passed as that argument.

Applying this to the Worker pattern

Now let's apply this to the QThreadPool worker pattern. Here's what happens step by step.

First, the Worker class is initialized with a function and optional keyword arguments:

python
class Worker(QRunnable):
    def __init__(self, fn, **kwargs):
        super().__init__()
        self.fn = fn
        self.kwargs = kwargs
        self.signals = WorkerSignals()

At this point, self.kwargs is a dictionary containing whatever keyword arguments were passed when the Worker was created. It might be empty, or it might already contain some values.

Next, before the function runs, the progress callback signal is added to the dictionary:

python
self.kwargs['progress_callback'] = self.signals.progress

Now self.kwargs has an entry where the key is the string "progress_callback" and the value is the self.signals.progress signal object.

Finally, when the worker runs, the function is called with keyword unpacking:

python
result = self.fn(**self.kwargs)

Python unpacks self.kwargs into keyword arguments. Since one of the keys is "progress_callback", this becomes:

python
result = self.fn(progress_callback=self.signals.progress)

And that's how progress_callback arrives as a named parameter in your function. The function signature def execute_this_fn(progress_callback): receives the signal object, and inside the function you can call progress_callback.emit(...) to report progress back to the main thread.

A standalone demonstration

To make this even clearer, here's a minimal example that demonstrates the same unpacking pattern without any Qt code:

python
def execute_this_fn(progress_callback):
    for n in range(5):
        progress_callback(n * 100 / 4)


def my_progress_handler(value):
    print(f"Progress: {value}%")


# Build the kwargs dictionary, just like the Worker does.
kwargs = {}
kwargs["progress_callback"] = my_progress_handler

# Call the function with keyword unpacking.
execute_this_fn(**kwargs)

Running this prints:

python
Progress: 0.0%
Progress: 25.0%
Progress: 50.0%
Progress: 75.0%
Progress: 100.0%

The function my_progress_handler is stored in the dictionary under the key "progress_callback". When we call execute_this_fn(**kwargs), Python matches that key to the progress_callback parameter in the function definition. The function then calls progress_callback(...), which in turn calls my_progress_handler(...).

You can pass additional arguments too

One reason this pattern is so useful is that you can pass your own keyword arguments to the Worker alongside the automatically-added progress_callback. For example:

python
worker = Worker(execute_this_fn, url="https://example.com", retries=3)

Inside __init__, self.kwargs would start as {"url": "https://example.com", "retries": 3}. After the progress callback is added, it becomes:

python
{"url": "https://example.com", "retries": 3, "progress_callback": <signal>}

When self.fn(**self.kwargs) runs, it's equivalent to:

python
self.fn(url="https://example.com", retries=3, progress_callback=<signal>)

Your function just needs matching parameter names:

python
def execute_this_fn(url, retries, progress_callback):
    # All three arguments are available here.
    ...

This makes the Worker class very flexible — it can call any function with any combination of keyword arguments, and the progress callback is always included automatically.

Complete working example with PyQt6

Here's a complete working example that puts it all together using QThreadPool. If you're new to PyQt6, you may want to first read through creating your first window and signals and slots before diving into multithreading.

python
import sys
import time
import traceback

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


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

    finished = pyqtSignal()
    error = pyqtSignal(tuple)
    result = pyqtSignal(object)
    progress = pyqtSignal(float)


class Worker(QRunnable):
    """Worker thread using QRunnable."""

    def __init__(self, fn, **kwargs):
        super().__init__()
        self.fn = fn
        self.kwargs = kwargs
        self.signals = WorkerSignals()

        # Add the progress callback to kwargs so it gets
        # passed to the function via keyword unpacking.
        self.kwargs["progress_callback"] = self.signals.progress

    @pyqtSlot()
    def run(self):
        try:
            result = self.fn(**self.kwargs)
        except Exception:
            traceback.print_exc()
            exctype, value = sys.exc_info()[:2]
            self.signals.error.emit(
                (exctype, value, traceback.format_exc())
            )
        else:
            self.signals.result.emit(result)
        finally:
            self.signals.finished.emit()


def execute_this_fn(progress_callback):
    """A long-running function that reports progress."""
    for n in range(5):
        time.sleep(1)
        progress_callback.emit(n * 100 / 4)
    return "Task complete!"


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

        self.threadpool = QThreadPool()

        layout = QVBoxLayout()

        self.label = QLabel("Press the button to start a task.")
        layout.addWidget(self.label)

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

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

    def start_task(self):
        worker = Worker(execute_this_fn)
        worker.signals.progress.connect(self.update_progress)
        worker.signals.result.connect(self.task_finished)
        self.threadpool.start(worker)

    def update_progress(self, value):
        self.label.setText(f"Progress: {value:.0f}%")

    def task_finished(self, result):
        self.label.setText(result)


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

When you run this and click the button, the label updates with progress percentages as the background task runs. The progress_callback parameter in execute_this_fn is the self.signals.progress signal — connected to update_progress on the main window — and it got there entirely through keyword unpacking of self.kwargs.

For more on using @pyqtSlot() decorator seen on the run method, see What does @pyqtSlot() do?

The complete guide to packaging Python GUI applications with PyInstaller.
[[ discount.discount_pc ]]% OFF for the next [[ discount.duration ]] [[discount.description ]] with the code [[ discount.coupon_code ]]

Purchasing Power Parity

Developers in [[ country ]] get [[ discount.discount_pc ]]% OFF on all books & courses with code [[ discount.coupon_code ]]
Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak
Martin Fitzpatrick

Understanding Python Keyword Unpacking with QThreadPool Progress Callbacks in PyQt6 was written by Martin Fitzpatrick.

Martin Fitzpatrick is the creator of Python GUIs, and has been developing Python/Qt applications for the past 12+ years. He has written a number of popular Python books and provides Python software development & consulting for teams and startups.