How do I use
QTreeViewwith a custom model in PyQt6? There's noQAbstractTreeModel, so how do I implementQAbstractItemModelto 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
- Understanding the Tree Model Structure
- The TreeItem Helper Class
- Building the Custom Tree Model
- Putting It All Together
- How the Pieces Fit Together
- Responding to Selection Changes
- Making Items Editable
- Adding and Removing Items
- Complete Editable Example with Add/Remove
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.
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.
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.
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:
index(row, column, parent)— return aQModelIndexfor the given positionparent(index)— return the parentQModelIndexof the given indexrowCount(parent)— return the number of children under the given parentcolumnCount(parent)— return the number of columnsdata(index, role)— return the data for a given index and role
Let's build the complete model:
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:
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:
TreeItemstores 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.TreeModelsubclassesQAbstractItemModeland translates between Qt'sQModelIndexsystem and yourTreeItemobjects. It answers Qt's questions about the structure and content of your data.QTreeViewis 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:
tree.selectionModel().currentChanged.connect(self.on_current_changed)
Then define the slot:
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:
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:
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:
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:
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