QTreeView with QAbstractItemModel in PyQt6

Build hierarchical tree displays using custom models
Heads up! You've already completed this tutorial.

How do I use QTreeView with a custom model in PyQt6? There's no QAbstractTreeModel, so how do I implement QAbstractItemModel to display hierarchical (tree) data?

If you've used QTableView with QAbstractTableModel, you already know the basics of Qt's model/view architecture. But when it comes to displaying hierarchical data — data with parent-child relationships — you need QTreeView. And since Qt doesn't provide a ready-made QAbstractTreeModel, you need to subclass QAbstractItemModel yourself.

This can feel daunting at first, but once you understand the structure, it follows a clear pattern. In this tutorial, we'll walk through everything step by step: from displaying a simple tree, to building a fully custom tree model that you can adapt to your own data.

A Simple QTreeView with QStandardItemModel

Before we dive into custom models, let's start with something quick and visual. Qt provides QStandardItemModel, a general-purpose model that works with QTreeView out of the box. This is a great way to see a tree in action without writing much code.

python
import sys

from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QTreeView
)
from PyQt6.QtGui import QStandardItemModel, QStandardItem


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

        tree = QTreeView(self)
        self.setCentralWidget(tree)

        model = QStandardItemModel()
        model.setHorizontalHeaderLabels(["Name", "Description"])

        # Add some items.
        parent_item = QStandardItem("Mammals")
        parent_item_desc = QStandardItem("Warm-blooded animals")
        model.appendRow([parent_item, parent_item_desc])

        child1 = QStandardItem("Cat")
        child1_desc = QStandardItem("A small domesticated carnivore")
        parent_item.appendRow([child1, child1_desc])

        child2 = QStandardItem("Dog")
        child2_desc = QStandardItem("A loyal domesticated carnivore")
        parent_item.appendRow([child2, child2_desc])

        parent_item2 = QStandardItem("Reptiles")
        parent_item2_desc = QStandardItem("Cold-blooded animals")
        model.appendRow([parent_item2, parent_item2_desc])

        child3 = QStandardItem("Snake")
        child3_desc = QStandardItem("A legless reptile")
        parent_item2.appendRow([child3, child3_desc])

        tree.setModel(model)
        tree.expandAll()


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

Run this and you'll see a tree with expandable nodes, two columns, and parent-child relationships displayed visually. QStandardItemModel stores data internally, so it's convenient for small or static trees.

But what if your data comes from somewhere else — a database, an API, a file? Or what if you need more control over how data is structured and accessed? That's where a custom QAbstractItemModel comes in.

Understanding the Tree Model Structure

In a table model (QAbstractTableModel), every item lives in a flat grid of rows and columns. In a tree model, items also have parents. A top-level item's parent is the "root" of the model (an invalid QModelIndex). A child item's parent is another item in the tree.

Qt's model/view system uses QModelIndex objects to refer to items. Each QModelIndex carries three pieces of information:

  • row — the item's row within its parent
  • column — the column
  • internalPointer (or internalId) — a reference to the underlying data object for that item

When you subclass QAbstractItemModel, Qt will call methods on your model asking questions like:

  • "How many rows does this parent have?"rowCount(parent)
  • "How many columns does this parent have?"columnCount(parent)
  • "What data is at this index?"data(index, role)
  • "Give me an index for row R, column C, under this parent"index(row, column, parent)
  • "What is the parent of this index?"parent(index)

The index() and parent() methods are what make tree models different from table models. They define the hierarchy.

Create GUI Applications with Python & Qt6 by Martin Fitzpatrick — (PyQt6 Edition) The hands-on guide to making apps with Python — Save time and build better with this book. Over 15K copies sold.

Get the book

The TreeItem Helper Class

To keep our model clean, we'll create a small helper class called TreeItem. Each TreeItem represents one node in the tree. It holds its own data, a reference to its parent item, and a list of child items.

python
class TreeItem:
    def __init__(self, data, parent=None):
        self.item_data = data
        self.parent_item = parent
        self.child_items = []

    def appendChild(self, child):
        self.child_items.append(child)

    def child(self, row):
        if 0 <= row < len(self.child_items):
            return self.child_items[row]
        return None

    def childCount(self):
        return len(self.child_items)

    def columnCount(self):
        return len(self.item_data)

    def data(self, column):
        if 0 <= column < len(self.item_data):
            return self.item_data[column]
        return None

    def row(self):
        if self.parent_item:
            return self.parent_item.child_items.index(self)
        return 0

    def parent(self):
        return self.parent_item

Each TreeItem stores its data as a list — one entry per column. For example, ["Cat", "A small domesticated carnivore"] for a two-column tree. The row() method figures out which row this item occupies within its parent's list of children.

This class has no dependency on Qt at all. It's just plain Python, which makes it easy to test and reason about.

Building the Custom Tree Model

Now let's subclass QAbstractItemModel. There are five methods we must implement:

  1. index(row, column, parent) — return a QModelIndex for the given position
  2. parent(index) — return the parent QModelIndex of the given index
  3. rowCount(parent) — return the number of children under the given parent
  4. columnCount(parent) — return the number of columns
  5. data(index, role) — return the data for a given index and role

Let's build the complete model:

python
from PyQt6.QtCore import QAbstractItemModel, QModelIndex, Qt


class TreeModel(QAbstractItemModel):
    def __init__(self, data, parent=None):
        super().__init__(parent)
        self.root_item = TreeItem(["Name", "Description"])
        self._setup_model_data(data, self.root_item)

    def index(self, row, column, parent=QModelIndex()):
        if not self.hasIndex(row, column, parent):
            return QModelIndex()

        if not parent.isValid():
            parent_item = self.root_item
        else:
            parent_item = parent.internalPointer()

        child_item = parent_item.child(row)
        if child_item:
            return self.createIndex(row, column, child_item)
        return QModelIndex()

    def parent(self, index):
        if not index.isValid():
            return QModelIndex()

        child_item = index.internalPointer()
        parent_item = child_item.parent()

        if parent_item == self.root_item:
            return QModelIndex()

        return self.createIndex(parent_item.row(), 0, parent_item)

    def rowCount(self, parent=QModelIndex()):
        if parent.column() > 0:
            return 0

        if not parent.isValid():
            parent_item = self.root_item
        else:
            parent_item = parent.internalPointer()

        return parent_item.childCount()

    def columnCount(self, parent=QModelIndex()):
        if parent.isValid():
            return parent.internalPointer().columnCount()
        return self.root_item.columnCount()

    def data(self, index, role=Qt.ItemDataRole.DisplayRole):
        if not index.isValid():
            return None

        if role != Qt.ItemDataRole.DisplayRole:
            return None

        item = index.internalPointer()
        return item.data(index.column())

    def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
        if (
            orientation == Qt.Orientation.Horizontal
            and role == Qt.ItemDataRole.DisplayRole
        ):
            return self.root_item.data(section)
        return None

    def _setup_model_data(self, data, parent):
        for name, description, children in data:
            item = TreeItem([name, description], parent)
            parent.appendChild(item)
            if children:
                self._setup_model_data(children, item)

Let's walk through the methods that matter most here.

index()

This is called by the view whenever it needs to refer to a specific cell. It takes a row, column, and parent index. If the parent is invalid (i.e., the root), we use self.root_item. Otherwise, we get the parent's TreeItem via internalPointer(), look up the child at the given row, and create a new QModelIndex pointing to it using createIndex().

The call to self.hasIndex(row, column, parent) is a safety check — it ensures the requested row and column are within valid bounds.

parent()

This is the reverse of index(). Given a QModelIndex, it returns the index of that item's parent. We get the TreeItem from the index's internalPointer(), find its parent, and create an index for it. If the parent is the root item, we return an invalid QModelIndex() — that's how Qt represents "no parent" (i.e., a top-level item).

_setup_model_data()

This is our own method to populate the tree from a Python data structure. We're using a list of tuples where each tuple is (name, description, children), and children is either another list of tuples or an empty list. This recursive structure maps naturally to a tree.

Putting It All Together

Here's a complete working example that combines TreeItem, TreeModel, and a QTreeView:

python
import sys

from PyQt6.QtCore import QAbstractItemModel, QModelIndex, Qt
from PyQt6.QtWidgets import QApplication, QMainWindow, QTreeView


class TreeItem:
    def __init__(self, data, parent=None):
        self.item_data = data
        self.parent_item = parent
        self.child_items = []

    def appendChild(self, child):
        self.child_items.append(child)

    def child(self, row):
        if 0 <= row < len(self.child_items):
            return self.child_items[row]
        return None

    def childCount(self):
        return len(self.child_items)

    def columnCount(self):
        return len(self.item_data)

    def data(self, column):
        if 0 <= column < len(self.item_data):
            return self.item_data[column]
        return None

    def row(self):
        if self.parent_item:
            return self.parent_item.child_items.index(self)
        return 0

    def parent(self):
        return self.parent_item


class TreeModel(QAbstractItemModel):
    def __init__(self, data, parent=None):
        super().__init__(parent)
        self.root_item = TreeItem(["Name", "Description"])
        self._setup_model_data(data, self.root_item)

    def index(self, row, column, parent=QModelIndex()):
        if not self.hasIndex(row, column, parent):
            return QModelIndex()

        if not parent.isValid():
            parent_item = self.root_item
        else:
            parent_item = parent.internalPointer()

        child_item = parent_item.child(row)
        if child_item:
            return self.createIndex(row, column, child_item)
        return QModelIndex()

    def parent(self, index):
        if not index.isValid():
            return QModelIndex()

        child_item = index.internalPointer()
        parent_item = child_item.parent()

        if parent_item == self.root_item:
            return QModelIndex()

        return self.createIndex(parent_item.row(), 0, parent_item)

    def rowCount(self, parent=QModelIndex()):
        if parent.column() > 0:
            return 0

        if not parent.isValid():
            parent_item = self.root_item
        else:
            parent_item = parent.internalPointer()

        return parent_item.childCount()

    def columnCount(self, parent=QModelIndex()):
        if parent.isValid():
            return parent.internalPointer().columnCount()
        return self.root_item.columnCount()

    def data(self, index, role=Qt.ItemDataRole.DisplayRole):
        if not index.isValid():
            return None

        if role != Qt.ItemDataRole.DisplayRole:
            return None

        item = index.internalPointer()
        return item.data(index.column())

    def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
        if (
            orientation == Qt.Orientation.Horizontal
            and role == Qt.ItemDataRole.DisplayRole
        ):
            return self.root_item.data(section)
        return None

    def _setup_model_data(self, data, parent):
        for name, description, children in data:
            item = TreeItem([name, description], parent)
            parent.appendChild(item)
            if children:
                self._setup_model_data(children, item)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Custom QTreeView Model")
        self.resize(500, 400)

        data = [
            ("Mammals", "Warm-blooded animals", [
                ("Cat", "A small domesticated carnivore", []),
                ("Dog", "A loyal domesticated carnivore", [
                    ("Labrador", "A friendly breed", []),
                    ("Poodle", "A curly-haired breed", []),
                ]),
                ("Whale", "A large marine mammal", []),
            ]),
            ("Reptiles", "Cold-blooded animals", [
                ("Snake", "A legless reptile", []),
                ("Lizard", "A four-legged reptile", [
                    ("Gecko", "A small lizard", []),
                    ("Iguana", "A large herbivorous lizard", []),
                ]),
            ]),
            ("Birds", "Feathered animals", [
                ("Eagle", "A large bird of prey", []),
                ("Parrot", "A colorful talking bird", []),
            ]),
        ]

        model = TreeModel(data)
        tree = QTreeView(self)
        tree.setModel(model)
        tree.expandAll()
        tree.setAlternatingRowColors(True)

        # Resize columns to fit content.
        for col in range(model.columnCount()):
            tree.resizeColumnToContents(col)

        self.setCentralWidget(tree)


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

When you run this, you'll see a tree with three top-level categories (Mammals, Reptiles, Birds), each containing children, and some of those children have children of their own. You can collapse and expand nodes, and the two columns (Name and Description) display neatly.

How the Pieces Fit Together

Here's a summary of how QTreeView, TreeModel, and TreeItem work together:

  • TreeItem stores your actual data. Each item knows its parent and children. This is your data layer — you can adapt it to wrap any data structure you like.
  • TreeModel subclasses QAbstractItemModel and translates between Qt's QModelIndex system and your TreeItem objects. It answers Qt's questions about the structure and content of your data.
  • QTreeView is the visual widget. It asks the model for data and displays it. You never need to tell the view about your tree structure directly — it discovers everything through the model.

This separation means you can change how your data is stored (in TreeItem) without touching the view, or swap the view for a different one without changing the model. If you're working with tabular data instead, the same principles apply — see our guide to using QTableView with NumPy and Pandas.

Responding to Selection Changes

You'll often want to do something when the user clicks on an item in the tree. You can connect to the view's selectionModel() to respond to selection changes using signals and slots:

python
tree.selectionModel().currentChanged.connect(self.on_current_changed)

Then define the slot:

python
def on_current_changed(self, current, previous):
    item = current.internalPointer()
    if item:
        print(f"Selected: {item.data(0)} - {item.data(1)}")

This retrieves the TreeItem from the index and accesses its data directly.

Making Items Editable

To make your tree editable, you need to do two things in your model: implement setData() and return the right flags from flags().

Add these methods to TreeModel:

python
def flags(self, index):
    if not index.isValid():
        return Qt.ItemFlag.NoItemFlags
    return (
        Qt.ItemFlag.ItemIsEnabled
        | Qt.ItemFlag.ItemIsSelectable
        | Qt.ItemFlag.ItemIsEditable
    )

def setData(self, index, value, role=Qt.ItemDataRole.EditRole):
    if role != Qt.ItemDataRole.EditRole:
        return False

    item = index.internalPointer()
    if item:
        item.item_data[index.column()] = value
        self.dataChanged.emit(index, index, [role])
        return True
    return False

You also need to update data() to respond to EditRole:

python
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
    if not index.isValid():
        return None

    if role not in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole):
        return None

    item = index.internalPointer()
    return item.data(index.column())

With these changes, double-clicking any cell in the tree will open an editor, and your changes will be stored in the TreeItem.

Adding and Removing Items

To dynamically add or remove items, you need to use beginInsertRows()/endInsertRows() and beginRemoveRows()/endRemoveRows() to let the view know the model is changing. Here are two methods you can add to TreeModel:

python
def addChild(self, parent_index, name, description):
    if not parent_index.isValid():
        parent_item = self.root_item
    else:
        parent_item = parent_index.internalPointer()

    row = parent_item.childCount()
    self.beginInsertRows(parent_index, row, row)
    new_item = TreeItem([name, description], parent_item)
    parent_item.appendChild(new_item)
    self.endInsertRows()

def removeChild(self, index):
    if not index.isValid():
        return

    item = index.internalPointer()
    parent_item = item.parent()

    if parent_item is None:
        return

    if parent_item == self.root_item:
        parent_index = QModelIndex()
    else:
        parent_index = self.createIndex(parent_item.row(), 0, parent_item)

    row = item.row()
    self.beginRemoveRows(parent_index, row, row)
    parent_item.child_items.pop(row)
    self.endRemoveRows()

The beginInsertRows() and endInsertRows() calls bracket the actual data modification. This pattern tells the view exactly what's changing, so it can update itself efficiently.

Complete Editable Example with Add/Remove

Here's a full example that brings everything together — an editable tree with buttons to add and remove items:

python
import sys

from PyQt6.QtCore import QAbstractItemModel, QModelIndex, Qt
from PyQt6.QtWidgets import (
    QApplication, QHBoxLayout, QMainWindow, QPushButton,
    QTreeView, QVBoxLayout, QWidget,
)


class TreeItem:
    def __init__(self, data, parent=None):
        self.item_data = data
        self.parent_item = parent
        self.child_items = []

    def appendChild(self, child):
        self.child_items.append(child)

    def child(self, row):
        if 0 <= row < len(self.child_items):
            return self.child_items[row]
        return None

    def childCount(self):
        return len(self.child_items)

    def columnCount(self):
        return len(self.item_data)

    def data(self, column):
        if 0 <= column < len(self.item_data):
            return self.item_data[column]
        return None

    def row(self):
        if self.parent_item:
            return self.parent_item.child_items.index(self)
        return 0

    def parent(self):
        return self.parent_item


class TreeModel(QAbstractItemModel):
    def __init__(self, data, parent=None):
        super().__init__(parent)
        self.root_item = TreeItem(["Name", "Description"])
        self._setup_model_data(data, self.root_item)

    def index(self, row, column, parent=QModelIndex()):
        if not self.hasIndex(row, column, parent):
            return QModelIndex()

        if not parent.isValid():
            parent_item = self.root_item
        else:
            parent_item = parent.internalPointer()

        child_item = parent_item.child(row)
        if child_item:
            return self.createIndex(row, column, child_item)
        return QModelIndex()

    def parent(self, index):
        if not index.isValid():
            return QModelIndex()

        child_item = index.internalPointer()
        parent_item = child_item.parent()

        if parent_item == self.root_item:
            return QModelIndex()

        return self.createIndex(parent_item.row(), 0, parent_item)

    def rowCount(self, parent=QModelIndex()):
        if parent.column() > 0:
            return 0

        if not parent.isValid():
            parent_item = self.root_item
        else:
            parent_item = parent.internalPointer()

        return parent_item.childCount()

    def columnCount(self, parent=QModelIndex()):
        if parent.isValid():
            return parent.internalPointer().columnCount()
        return self.root_item.columnCount()

    def data(self, index, role=Qt.ItemDataRole.DisplayRole):
        if not index.isValid():
            return None

        if role not in (
            Qt.ItemDataRole.DisplayRole,
            Qt.ItemDataRole.EditRole,
        ):
            return None

        item = index.internalPointer()
        return item.data(index.column())

    def setData(self, index, value, role=Qt.ItemDataRole.EditRole):
        if role != Qt.ItemDataRole.EditRole:
            return False

        item = index.internalPointer()
        if item:
            item.item_data[index.column()] = value
            self.dataChanged.emit(index, index, [role])
            return True
        return False

    def flags(self, index):
        if not index.isValid():
            return Qt.ItemFlag.NoItemFlags
        return (
            Qt.ItemFlag.ItemIsEnabled
            | Qt.ItemFlag.ItemIsSelectable
            | Qt.ItemFlag.ItemIsEditable
        )

    def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
        if (
            orientation == Qt.Orientation.Horizontal
            and role == Qt.ItemDataRole.DisplayRole
        ):
            return self.root_item.data(section)
        return None

    def addChild(self, parent_index, name, description):
        if not parent_index.isValid():
            parent_item = self.root_item
        else:
            parent_item = parent_index.internalPointer()

        row = parent_item.childCount()
        self.beginInsertRows(parent_index, row, row)
        new_item = TreeItem([name, description], parent_item)
        parent_item.appendChild(new_item)
        self.endInsertRows()

    def removeChild(self, index):
        if not index.isValid():
            return

        item = index.internalPointer()
        parent_item = item.parent()

        if parent_item is None:
            return

        if parent_item == self.root_item:
            parent_index = QModelIndex()
        else:
            parent_index = self.createIndex(
                parent_item.row(), 0, parent_item
            )

        row = item.row()
        self.beginRemoveRows(parent_index, row, row)
        parent_item.child_items.pop(row)
        self.endRemoveRows()

    def _setup_model_data(self, data, parent):
        for name, description, children in data:
            item = TreeItem([name, description], parent)
            parent.appendChild(item)
            if children:
                self._setup_model_data(children, item)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Editable QTreeView")
        self.resize(550, 450)

        data = [
            ("Mammals", "Warm-blooded animals", [
                ("Cat", "A small domesticated carnivore", []),
                ("Dog", "A loyal domesticated carnivore", [
                    ("Labrador", "A friendly breed", []),
                    ("Poodle", "A curly-haired breed", []),
                ]),
            ]),
            ("Reptiles", "Cold-blooded animals", [
                ("Snake", "A legless reptile", []),
                ("Lizard", "A four-legged reptile", []),
            ]),
        ]

        self.model = TreeModel(data)
        self.tree = QTreeView()
        self.tree.setModel(self.model)
        self.tree.expandAll()
        self.tree.setAlternatingRowColors(True)

        for col in range(self.model.columnCount()):
            self.tree.resizeColumnToContents(col)

        add_button = QPushButton("Add Child")
        add_button.clicked.connect(self.add_child)

        remove_button = QPushButton("Remove Selected")
        remove_button.clicked.connect(self.remove_selected)

        button_layout = QHBoxLayout()
        button_layout.addWidget(add_button)
        button_layout.addWidget(remove_button)

        layout = QVBoxLayout()
        layout.addWidget(self.tree)
        layout.addLayout(button_layout)

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

    def add_child(self):
        indexes = self.tree.selectedIndexes()
        if indexes:
            # Use the first selected index's row-level index (column 0).
            parent_index = indexes[0].siblingAtColumn(0)
        else:
            parent_index = QModelIndex()

        self.model.addChild(parent_index, "New Item", "Description")
        self.tree.expandAll()

    def remove_selected(self):
        indexes = self.tree.selectedIndexes()
        if indexes:
            self.model.removeChild(indexes[0])


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

You can now:

  • Expand/collapse tree nodes by clicking the arrows
  • Double-click any cell to edit it in place
  • Select an item and click "Add Child" to add a new child under it
  • Select an item and click "Remove Selected" to delete it
The complete guide to packaging Python GUI applications with PyInstaller.
[[ discount.discount_pc ]]% OFF for the next [[ discount.duration ]] [[discount.description ]] with the code [[ discount.coupon_code ]]

Purchasing Power Parity

Developers in [[ country ]] get [[ discount.discount_pc ]]% OFF on all books & courses with code [[ discount.coupon_code ]]
Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak
Martin Fitzpatrick

QTreeView with QAbstractItemModel in PyQt6 was written by Martin Fitzpatrick.

Martin Fitzpatrick is the creator of Python GUIs, and has been developing Python/Qt applications for the past 12+ years. He has written a number of popular Python books and provides Python software development & consulting for teams and startups.