I'm running a long-running function in a separate thread using
QThreadPooland aQRunnableworker. 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:
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:
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:
# 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.
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:
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:
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:
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.