How does adding
progress_callbacktoself.kwargsin aQThreadPoolworker connect it to theexecute_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:
# Add the callback to our kwargs
self.kwargs['progress_callback'] = self.signals.progress
And then a function that receives it:
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:
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.
def greet(name, greeting):
print(f"{greeting}, {name}!")
my_args = {
"name": "Alice",
"greeting": "Hello",
}
greet(**my_args)
This outputs:
Hello, Alice!
When Python sees greet(**my_args), it unpacks the dictionary so the call becomes equivalent to:
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:
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:
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:
result = self.fn(**self.kwargs)
Python unpacks self.kwargs into keyword arguments. Since one of the keys is "progress_callback", this becomes:
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:
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:
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:
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:
{"url": "https://example.com", "retries": 3, "progress_callback": <signal>}
When self.fn(**self.kwargs) runs, it's equivalent to:
self.fn(url="https://example.com", retries=3, progress_callback=<signal>)
Your function just needs matching parameter names:
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.
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?