Passing Arguments to Threaded Functions in PyQt6

Understanding the difference between passing a function and calling a function when using QThreadPool and QRunnable
Heads up! You've already completed this tutorial.

I'm running a long-running function in a separate thread using QThreadPool and a QRunnable worker. When I pass the function without arguments, it runs correctly in the background. But when I try to pass arguments, the GUI freezes until the function finishes. What am I doing wrong?

This is a very common stumbling block when working with threads in PyQt6, and the good news is that it has a simple fix. The problem comes down to the difference between passing a function and calling a function — two things that look similar in Python but behave very differently.

The Setup

Let's say you have a basic app with a main window, a push button, and a status bar. When the button is pressed, you want to start a long-running task on a background thread. That task should emit a signal to update the status bar, then carry on with its work.

Here's a minimal version of this pattern using QThreadPool and QRunnable, based on the approach covered in our Multithreading PyQt6 applications with QThreadPool tutorial:

python
import sys
import time

from PyQt6.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton


class WorkerSignals(QObject):
    status = pyqtSignal(str)


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

    @pyqtSlot()
    def run(self):
        self.fn(*self.args, **self.kwargs)


class Model(QObject):
    def __init__(self):
        super().__init__()
        self.signals = WorkerSignals()

    def do_this(self):
        self.signals.status.emit("Working on it...")
        time.sleep(5)  # Simulating a long-running task
        self.signals.status.emit("Done!")


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

        self.model = Model()
        self.model.signals.status.connect(self.update_status_bar)

        button = QPushButton("Run Task")
        button.pressed.connect(self.call_do_this)
        self.setCentralWidget(button)

        self.threadpool = QThreadPool()

    def call_do_this(self):
        worker = Worker(self.model.do_this)
        self.threadpool.start(worker)

    def update_status_bar(self, msg):
        self.statusBar().showMessage(msg)


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

This works perfectly. The function runs on a background thread, the status bar updates, and the GUI stays responsive.

Where Things Go Wrong

Now suppose do_this needs to accept a message as an argument. You might naturally try this:

python
def call_do_this(self):
    worker = Worker(self.model.do_this("This is a message"))
    self.threadpool.start(worker)

Suddenly the GUI freezes for the entire duration of the task. The status bar doesn't update until everything is finished. What happened?

Passing a Function vs. Calling a Function

Look closely at what's being passed to Worker:

python
# This CALLS the function immediately and passes its return value
Worker(self.model.do_this("This is a message"))

# This PASSES the function itself, along with the argument separately
Worker(self.model.do_this, "This is a message")

When you write self.model.do_this("This is a message") with parentheses and arguments, Python executes that function right then and there, on the main thread. The function runs to completion — including the long-running part — and then its return value (which is None, since the function doesn't explicitly return anything) gets passed to Worker.

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

Find out More

So Worker receives None as its fn parameter. The actual work already happened on the main thread, blocking the GUI the whole time.

When you write self.model.do_this without parentheses, you're passing a reference to the function itself. The Worker then calls it later, inside run(), which executes on a background thread. The arguments "This is a message" are passed separately and stored until run() is called.

The Fix

Pass the function and its arguments as separate items to Worker:

python
def call_do_this(self):
    worker = Worker(self.model.do_this, "This is a message")
    self.threadpool.start(worker)

The Worker.__init__ method is set up to handle exactly this pattern using *args and **kwargs:

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

    @pyqtSlot()
    def run(self):
        self.fn(*self.args, **self.kwargs)

The function reference is stored in self.fn, and any additional positional or keyword arguments are captured and stored in self.args and self.kwargs. When run() is called on the worker thread, it calls the function with those stored arguments.

Complete Working Example

Here's a full working example that demonstrates passing arguments to a threaded function, with proper signal handling for status updates:

python
import sys
import time

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."""
    status = pyqtSignal(str)
    finished = pyqtSignal()


class Worker(QRunnable):
    """Worker thread using QRunnable to run a function in the background."""

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

    @pyqtSlot()
    def run(self):
        self.fn(*self.args, **self.kwargs)


class Model(QObject):
    """Holds the application logic."""

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

    def do_this(self, msg):
        self.signals.status.emit(msg)
        # Simulate a long-running task
        for i in range(5):
            time.sleep(1)
            self.signals.status.emit(f"Processing step {i + 1} of 5...")
        self.signals.status.emit("Task complete!")
        self.signals.finished.emit()


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

        self.model = Model()
        self.model.signals.status.connect(self.update_status_bar)
        self.model.signals.finished.connect(self.task_finished)

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

        self.button = QPushButton("Run Task")
        self.button.pressed.connect(self.call_do_this)

        layout = QVBoxLayout()
        layout.addWidget(self.label)
        layout.addWidget(self.button)

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

        self.threadpool = QThreadPool()

    def call_do_this(self):
        self.button.setEnabled(False)
        # Pass the function and its argument separately
        worker = Worker(self.model.do_this, "Starting task...")
        self.threadpool.start(worker)

    def update_status_bar(self, msg):
        self.statusBar().showMessage(msg)
        self.label.setText(msg)

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


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

When you run this and click the button, you'll see the status bar and label update in real time as the background task progresses. The GUI stays responsive throughout because the work is happening on a separate thread.

Quick Reference

What you write What happens
Worker(self.model.do_this) Passes the function — it runs on the worker thread
Worker(self.model.do_this, "hello") Passes the function and an argument — both used on the worker thread
Worker(self.model.do_this("hello")) Calls the function immediately on the main thread, passes None to Worker - don't do this!

The key thing to remember is to pass the function by name, and pass the arguments separately after it. This keeps the function from running until the worker thread is ready.

If you're new to building PyQt6 applications and want to understand the fundamentals first, check out our guide to creating your first window with PyQt6.

Over 15,000 developers have bought Create GUI Applications with Python & Qt!
Create GUI Applications with Python & Qt6
Get the book

Downloadable ebook (PDF, ePub) & Complete Source code

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

Passing Arguments to Threaded Functions 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.