How to Pass Additional Arguments to a QRunnable Worker Function

Solving common errors when passing parameters to threaded Worker functions in PyQt5
Heads up! You've already completed this tutorial.

How do you pass additional arguments and keyword arguments to a QRunnable Worker function? Using a lambda or passing parameters directly results in errors like TypeError: <lambda>() got an unexpected keyword argument 'progress_callback' or TypeError: execute_this_fn() got multiple values for argument 'progress_callback'.

If you've been working with QThreadPool and a custom Worker class based on QRunnable in PyQt5, you've probably run into one of these confusing errors when trying to pass your own arguments to the worker function. The good news is that both errors have the same underlying cause, and once you understand how the Worker class works, the fix is straightforward.

Understanding the Worker class

Let's start by looking at a typical Worker class used for threading with QThreadPool. This pattern stores the function you want to run, along with any positional and keyword arguments you pass in:

python
class Worker(QRunnable):

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

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

    @pyqtSlot()
    def run(self):
        try:
            result = self.fn(*self.args, **self.kwargs)
        except:
            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()

Notice what's happening in __init__: the Worker automatically adds progress_callback as a keyword argument into self.kwargs. When run() is called, it passes all of self.args and self.kwargs — including progress_callback — to your function.

This means that every function you pass to the Worker will receive progress_callback as a keyword argument, whether you asked for it or not. This is where both errors come from.

Why the lambda error happens

If you try to use a lambda to pass arguments to your function:

python
worker = Worker(lambda: self.execute_this_fn(arg1, arg2))

The Worker will call your lambda and pass progress_callback as a keyword argument. But your lambda doesn't accept any keyword arguments — it takes no parameters at all. So Python raises:

python
TypeError: <lambda>() got an unexpected keyword argument 'progress_callback'

The lambda is "hiding" the real function from the Worker, so the Worker can't pass progress_callback through to it.

Why the "multiple values" error happens

If you try passing arguments directly:

python
worker = Worker(self.execute_this_fn, param1, param2)

And your function signature looks like this:

python
def execute_this_fn(self, param1, param2, progress_callback):
    ...

The Worker passes param1 and param2 as positional arguments, and then also passes progress_callback as a keyword argument. If the positions line up so that progress_callback has already been filled by a positional argument, Python raises:

python
TypeError: execute_this_fn() got multiple values for argument 'progress_callback'

The correct way to pass arguments

The solution is to pass your additional arguments as keyword arguments when creating the Worker, and make sure your function signature accepts progress_callback as a keyword argument too.

Here's how to set up your function:

python
def execute_this_fn(self, progress_callback, start=0, end=5):
    for n in range(start, end):
        time.sleep(0.1)
        progress_callback.emit(int((n / (end - start)) * 100))
    return "Done."

And here's how to create the Worker:

python
worker = Worker(self.execute_this_fn, start=2, end=10)

By passing start and end as keyword arguments, they won't collide with progress_callback. The Worker adds progress_callback to the kwargs dictionary, and your function receives all of them cleanly.

Accepting progress_callback even when you don't use it

Even if your function doesn't need to report progress, it will still receive progress_callback because the Worker always injects it. You can handle this by accepting **kwargs in your function signature to absorb any extra keyword arguments:

python
def execute_this_fn(self, name="World", **kwargs):
    time.sleep(2)
    return f"Hello, {name}!"

The **kwargs will quietly absorb progress_callback without causing an error. This is a clean approach when you don't need progress reporting.

Alternatively, you can explicitly include progress_callback in the signature and simply not use it:

python
def execute_this_fn(self, progress_callback, name="World"):
    time.sleep(2)
    return f"Hello, {name}!"

Either approach works fine.

Complete working example

Here's a full example that demonstrates passing extra arguments to a Worker function, with progress reporting:

python
import sys
import time
import traceback

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


class WorkerSignals(QObject):
    finished = pyqtSignal()
    error = pyqtSignal(tuple)
    result = pyqtSignal(object)
    progress = pyqtSignal(int)


class Worker(QRunnable):

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

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

    @pyqtSlot()
    def run(self):
        try:
            result = self.fn(*self.args, **self.kwargs)
        except:
            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()


class MainWindow(QMainWindow):

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

        self.threadpool = QThreadPool()

        layout = QVBoxLayout()

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

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

        button = QPushButton("Start (0 to 10)")
        button.clicked.connect(lambda: self.start_worker(start=0, end=10))
        layout.addWidget(button)

        button2 = QPushButton("Start (5 to 15)")
        button2.clicked.connect(lambda: self.start_worker(start=5, end=15))
        layout.addWidget(button2)

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

    def execute_this_fn(self, progress_callback, start=0, end=10):
        total_steps = end - start
        for n in range(start, end):
            time.sleep(0.3)
            progress = int(((n - start + 1) / total_steps) * 100)
            progress_callback.emit(progress)
        return f"Counted from {start} to {end}!"

    def start_worker(self, start=0, end=10):
        worker = Worker(self.execute_this_fn, start=start, end=end)
        worker.signals.result.connect(self.print_output)
        worker.signals.progress.connect(self.update_progress)
        worker.signals.finished.connect(self.thread_complete)

        self.progress_bar.setValue(0)
        self.label.setText(f"Counting from {start} to {end}...")
        self.threadpool.start(worker)

    def print_output(self, result):
        self.label.setText(str(result))

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

    def thread_complete(self):
        print("Thread complete!")


app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())

When you run this, you'll see two buttons. Each one starts the same worker function but with different start and end values. The progress bar updates as the function runs, and the result is displayed in the label when it finishes.

A note about running GUI elements from threads

One of the follow-up questions in the forum asked about running a QDialog from a Worker thread. This is something to be very careful about: you should never create or interact with Qt widgets from any thread other than the main thread**_. Qt's GUI components are not thread-safe, and doing this will lead to random crashes.

If you need to run a long task without freezing your GUI, use the Worker/QThreadPool pattern shown above to run the processing in a background thread, and use signals to communicate results back to the main thread. The main thread then updates the UI. This keeps your interface responsive while keeping all widget interactions safely on the main thread.

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

Packaging Python Applications with PyInstaller by Martin Fitzpatrick

This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.

More info Get the book

Martin Fitzpatrick

How to Pass Additional Arguments to a QRunnable Worker Function 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.