Using QThreadPool.start() with a Simple Function in PySide6

Run background tasks without creating a QRunnable by passing a callable directly to QThreadPool
Heads up! You've already completed this tutorial.

In PyQt6, QThreadPool.start() accepts either a QRunnable or a plain Python callable. Does PySide6 support passing a callable directly to QThreadPool.start(), so you don't have to create a QRunnable subclass for simple background tasks?

Good news — PySide6 now supports passing a plain Python function (or any callable) directly to QThreadPool.start(). This means you can run background tasks without the extra boilerplate of subclassing QRunnable. Let's walk through how it works and when you might still want to use QRunnable.

QThreadPool.start() Signatures

QThreadPool.start() accepts two types of argument:

  1. A QRunnable instance — the traditional approach where you subclass QRunnable and implement the run() method.
  2. A Python callable — any function, method, or callable object with no arguments.

Both signatures also accept an optional priority parameter (default 0), which controls the order in which tasks are picked up from the pool's queue.

python
# Signature 1: QRunnable
QThreadPool.globalInstance().start(my_runnable, priority=0)

# Signature 2: Callable
QThreadPool.globalInstance().start(my_function, priority=0)

The callable signature is perfect for simple, fire-and-forget background work where you don't need the full structure of a QRunnable subclass.

Passing a Function to QThreadPool.start()

Here's a minimal example that runs a function in the background using QThreadPool:

python
import sys
import time

from PySide6.QtCore import QThreadPool
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget


def long_running_task():
    """Simulate a task that takes a few seconds."""
    print("Task started")
    time.sleep(3)
    print("Task finished")


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("QThreadPool Function Demo")

        self.thread_pool = QThreadPool.globalInstance()
        print(f"Max thread count: {self.thread_pool.maxThreadCount()}")

        button = QPushButton("Run background task")
        button.clicked.connect(self.start_task)

        layout = QVBoxLayout()
        layout.addWidget(button)

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

    def start_task(self):
        self.thread_pool.start(long_running_task)


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

Click the button, and long_running_task runs on a background thread — no QRunnable needed. The UI stays responsive while the task sleeps for three seconds.

Using a Lambda or Method

Because start() accepts any callable with no arguments, you can also pass a lambda or a bound method:

python
# Using a lambda
self.thread_pool.start(lambda: print("Hello from a thread!"))

# Using a bound method
self.thread_pool.start(self.do_work)

If your function needs arguments, wrap it in a lambda:

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

Downloadable ebook (PDF, ePub) & Complete Source code

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

self.thread_pool.start(lambda: greet("World"))

When to Still Use QRunnable

Passing a plain function is convenient, but there are situations where subclassing QRunnable is the better choice:

  • You need to emit signalsQRunnable doesn't inherit from QObject, but you can attach a signals object to it and emit signals to send progress updates or results back to the main thread.
  • You want to manage task lifecycleQRunnable has setAutoDelete() and other methods that give you finer control.
  • You're reusing the same task pattern — wrapping the logic in a QRunnable subclass keeps things organized when you have multiple task types.

For a full walkthrough of using QRunnable with QThreadPool including signal handling and progress reporting, see the Multithreading PySide6 applications with QThreadPool tutorial.

Here's a quick comparison to illustrate:

python
from PySide6.QtCore import QRunnable, QThreadPool


class MyTask(QRunnable):
    def run(self):
        print("Running via QRunnable")


# Using QRunnable
pool = QThreadPool.globalInstance()
pool.start(MyTask())

# Using a plain function — same result, less code
pool.start(lambda: print("Running via callable"))

Complete Working Example

Here's a more complete example that demonstrates running multiple background tasks by passing functions to QThreadPool.start(). Each click queues a new task, and you can see them executing concurrently:

python
import sys
import time

from PySide6.QtCore import QThreadPool
from PySide6.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QWidget,
)


def background_task(task_id):
    """Simulate work with a unique task ID."""
    print(f"Task {task_id}: started")
    time.sleep(3)
    print(f"Task {task_id}: finished")


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("QThreadPool Callable Demo")
        self.resize(300, 150)

        self.thread_pool = QThreadPool.globalInstance()
        self.task_counter = 0

        self.label = QLabel("Click the button to queue tasks")
        self.label.setStyleSheet("padding: 10px;")

        button = QPushButton("Queue background task")
        button.clicked.connect(self.queue_task)

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

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

    def queue_task(self):
        self.task_counter += 1
        task_id = self.task_counter
        self.label.setText(f"Queued task {task_id}")

        # Pass a callable directly — no QRunnable subclass needed
        self.thread_pool.start(lambda tid=task_id: background_task(tid))


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

Notice the tid=task_id default argument in the lambda. This captures the current value of task_id at the time the lambda is created, rather than sharing a reference to the variable that keeps changing as you click. This is a common Python pattern when creating closures in a loop or repeated callback.

Run this example, click the button a few times in quick succession, and watch the terminal output — you'll see tasks starting and finishing concurrently, all managed by the thread pool, and all without writing a single QRunnable subclass. If you're building a full PySide6 application, you may also want to set up your window with layouts and widgets to provide a richer interface around your background tasks.

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

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

Using QThreadPool.start() with a Simple Function in PySide6 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.