Create a tree using threads

How to populate a QTreeView using QThreadPool and QRunnable without blocking the GUI
Heads up! You've already completed this tutorial.

I'm trying to create a PySide6 application that fetches data from an API and presents it in a QTreeView. Since the API calls take a long time, I want to retrieve the data using a thread (QThreadPool/QRunnable). When I try to build a QStandardItemModel inside a QRunnable and emit it back to the main thread via a signal, I get a TypeError: 'Signal' object is not callable. How can I correctly populate a QTreeView from a background thread?

There are a couple of things going on in this question. First, there's a straightforward signal connection bug. Second — and more importantly — there's a deeper issue with creating Qt GUI objects (like QStandardItemModel and QStandardItem) from a background thread. Let's work through both and arrive at a clean, working solution.

Fixing the signal connection

The immediate error in the original code is on this line:

python
thread.signals.signal_QStandardItemModel(self.treeAction)

To connect a signal to a slot in PySide6, you need to use .connect():

python
thread.signals.result.connect(self.treeAction)

Without .connect(), Python interprets the parentheses as a function call on the Signal object itself, which gives you the TypeError: 'Signal' object is not callable message. If you're unfamiliar with how signals and slots work in PySide6, see our complete guide to PySide6 signals, slots and events.

The bigger problem: Qt objects and threads

Fixing the connection syntax will get rid of that error, but you'll likely hit another problem: QStandardItemModel and QStandardItem are not thread-safe. Creating them in a background thread and then passing them to the main thread for use in a QTreeView can lead to crashes or unpredictable behavior.

The safe approach is to do the slow work (API calls, data processing) in the background thread, then send the raw data back to the main thread via a signal. The main thread receives that data and builds the model there. This keeps all Qt GUI work on the main thread, where it belongs.

Structuring the worker

Let's set up a QRunnable subclass that simulates slow data fetching. Instead of building QStandardItem objects, it collects the data into a plain Python structure (a list of dictionaries, or tuples, or whatever fits your needs) and emits it when done. For a full introduction to using QThreadPool and QRunnable for multithreading, see Multithreading PySide6 applications with QThreadPool.

python
import time
from PySide6.QtCore import QObject, QRunnable, Signal, Slot


class WorkerSignals(QObject):
    """Signals for the data-fetching worker."""
    result = Signal(list)   # Emits the fetched data as a Python list
    finished = Signal()


class DataWorker(QRunnable):
    """Fetch data in a background thread."""

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

    @Slot()
    def run(self):
        # Simulate slow API calls
        data = []

        time.sleep(2)  # Simulated delay
        data.append({"name": "A", "children": []})

        time.sleep(2)  # Another delay
        data.append({
            "name": "B",
            "children": [
                {"name": "C", "children": []},
            ],
        })

        data.append({"name": "D", "children": []})

        # Send the collected data back to the main thread
        self.signals.result.emit(data)
        self.signals.finished.emit()

The data list here is just plain Python — no Qt objects at all. The time.sleep() calls stand in for your real API requests. In your actual application you'd replace them with whatever network calls you need.

The complete guide to packaging Python GUI applications with PyInstaller.

Building the model on the main thread

When the result signal arrives on the main thread, we take the data and build the QStandardItemModel there. Here's a helper that recursively turns our nested dictionaries into tree items:

python
from PySide6.QtGui import QStandardItemModel, QStandardItem, QColor


def build_tree_model(data):
    """Create a QStandardItemModel from a nested list of dicts."""
    model = QStandardItemModel()
    root = model.invisibleRootItem()

    def add_items(parent, items):
        for entry in items:
            item = QStandardItem(entry["name"])
            item.setEditable(False)
            item.setForeground(QColor(0, 0, 0))
            parent.appendRow(item)
            if entry["children"]:
                add_items(item, entry["children"])

    add_items(root, data)
    return model

This keeps things tidy: the worker knows nothing about Qt widgets, and the model-building code knows nothing about threading. This approach follows the Model/View architecture pattern used throughout Qt.

Putting it all together

Here's the complete working example. There are three buttons: one starts a simple counter thread (to show the UI stays responsive), one populates the tree using a background thread, and one does it directly on the main thread (which freezes the UI — try it to see the difference).

python
import logging
import time

from PySide6.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot
from PySide6.QtGui import QColor, QStandardItem, QStandardItemModel
from PySide6.QtWidgets import (
    QApplication,
    QMainWindow,
    QPushButton,
    QTreeView,
    QVBoxLayout,
    QWidget,
)

logging.basicConfig(
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    level=logging.INFO,
)


# ── Worker signals ──────────────────────────────────────────────


class CounterSignals(QObject):
    """Signals for the counter worker."""
    progress = Signal(str)
    finished = Signal()


class DataSignals(QObject):
    """Signals for the data-fetching worker."""
    result = Signal(list)
    finished = Signal()


# ── Workers ─────────────────────────────────────────────────────


class CounterWorker(QRunnable):
    """A simple counter to prove the UI stays responsive."""

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

    @Slot()
    def run(self):
        for i in range(10):
            self.signals.progress.emit(str(i))
            time.sleep(0.3)
        self.signals.finished.emit()


class DataWorker(QRunnable):
    """Simulate slow data fetching in a background thread."""

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

    @Slot()
    def run(self):
        logging.info("DataWorker: starting data fetch")
        data = []

        # Simulate slow API responses
        time.sleep(2)
        data.append({"name": "A", "children": []})

        time.sleep(2)
        data.append({
            "name": "B",
            "children": [
                {"name": "C", "children": []},
            ],
        })

        data.append({"name": "D", "children": []})

        logging.info("DataWorker: data fetch complete")
        self.signals.result.emit(data)
        self.signals.finished.emit()


# ── Helpers ─────────────────────────────────────────────────────


def build_tree_model(data):
    """Turn a nested list of dicts into a QStandardItemModel."""
    model = QStandardItemModel()
    root = model.invisibleRootItem()

    def add_items(parent, items):
        for entry in items:
            item = QStandardItem(entry["name"])
            item.setEditable(False)
            item.setForeground(QColor(0, 0, 0))
            parent.appendRow(item)
            if entry["children"]:
                add_items(item, entry["children"])

    add_items(root, data)
    return model


# ── Main window ─────────────────────────────────────────────────


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Threaded QTreeView Example")

        layout = QVBoxLayout()

        self.btn_count = QPushButton("Start counting (threaded)")
        self.btn_tree_threaded = QPushButton("Generate tree (threaded)")
        self.btn_tree_blocking = QPushButton("Generate tree (blocking)")
        self.tree = QTreeView()

        layout.addWidget(self.btn_count)
        layout.addWidget(self.btn_tree_threaded)
        layout.addWidget(self.btn_tree_blocking)
        layout.addWidget(self.tree)

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

        self.btn_count.clicked.connect(self.start_counting)
        self.btn_tree_threaded.clicked.connect(self.start_tree_threaded)
        self.btn_tree_blocking.clicked.connect(self.start_tree_blocking)

        self.threadpool = QThreadPool()
        logging.info(
            "Thread pool with %d max threads", self.threadpool.maxThreadCount()
        )

    # ── Counter ─────────────────────────────────────────────────

    def start_counting(self):
        worker = CounterWorker()
        worker.signals.progress.connect(self.on_counter_progress)
        worker.signals.finished.connect(self.on_counter_finished)
        self.threadpool.start(worker)

    def on_counter_progress(self, value):
        logging.info("Counter: %s", value)

    def on_counter_finished(self):
        logging.info("Counter: done")

    # ── Tree (threaded) ─────────────────────────────────────────

    def start_tree_threaded(self):
        worker = DataWorker()
        worker.signals.result.connect(self.on_data_ready)
        worker.signals.finished.connect(self.on_data_finished)
        self.threadpool.start(worker)

    def on_data_ready(self, data):
        model = build_tree_model(data)
        self.tree.setModel(model)
        self.tree.expandAll()

    def on_data_finished(self):
        logging.info("Tree generation complete")

    # ── Tree (blocking, for comparison) ─────────────────────────

    def start_tree_blocking(self):
        """Build the tree on the main thread — the UI will freeze."""
        start = time.perf_counter()

        data = []
        time.sleep(2)
        data.append({"name": "A", "children": []})
        time.sleep(2)
        data.append({
            "name": "B",
            "children": [
                {"name": "C", "children": []},
            ],
        })
        data.append({"name": "D", "children": []})

        model = build_tree_model(data)
        self.tree.setModel(model)
        self.tree.expandAll()

        elapsed = time.perf_counter() - start
        logging.info("Blocking tree generated in %.2f s", elapsed)


# ── Run ─────────────────────────────────────────────────────────

app = QApplication([])
window = MainWindow()
window.show()
app.exec()

Try clicking "Start counting (threaded)" and then "Generate tree (threaded)" — you'll see the counter keeps ticking in the log while the tree data is being fetched. Click the blocking button instead and the entire window freezes for several seconds.

What to remember

Keep slow work in the worker, keep Qt objects on the main thread. Your QRunnable.run() method should do the heavy lifting — network requests, file I/O, data parsing — and produce plain Python data. Emit that data through a signal. When the main thread receives it, that's where you create QStandardItemModel, QStandardItem, and any other Qt objects.

This pattern scales well. As your API data grows more complex, you just adjust the data structure your worker emits and update build_tree_model() to match. The threading plumbing stays the same. For more threading best practices — including how to handle stopping, pausing, and managing multiple workers — see our threading dos and don'ts.

PyQt/PySide Office Hours 1:1 with Martin Fitzpatrick — Save yourself time and frustration. Get one on one help with your projects. Bring issues, bugs and questions about usability to architecture and maintainability, and leave with solutions.

60 mins ($195)

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

Create a tree using threads 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.