QTreeView shows hierarchical data (folders, categories, parent/child records) as an expandable tree. It displays a model: the quickest is QStandardItemModel, where you add QStandardItem rows under other items. This guide builds working PyQt6 tree views step by step: a basic tree, a tree from a nested dict, a file browser, checkboxes, search, a context menu, styling and a custom model. Every example is a complete program with a screenshot of the real window.
Tested with Python 3.12.5, PyQt6 6.11.0 (Qt 6.11.0) on Windows 11. Install PyQt6 with pip install PyQt6 (see install PyQt6). For the full reference, see the Qt 6 QTreeView documentation.
Basic QTreeView example
import sys
from PyQt6.QtGui import QStandardItem, QStandardItemModel
from PyQt6.QtWidgets import QApplication, QTreeView
app = QApplication(sys.argv)
model = QStandardItemModel()
model.setHorizontalHeaderLabels(["Name", "Type"])
fruits = QStandardItem("Fruits")
fruits.appendRow([QStandardItem("Apple"), QStandardItem("Pome")])
fruits.appendRow([QStandardItem("Mango"), QStandardItem("Drupe")])
vegetables = QStandardItem("Vegetables")
vegetables.appendRow([QStandardItem("Carrot"), QStandardItem("Root")])
model.appendRow([fruits, QStandardItem("Category")])
model.appendRow([vegetables, QStandardItem("Category")])
tree = QTreeView()
tree.setModel(model)
tree.expandAll()
tree.setColumnWidth(0, 200)
tree.setWindowTitle("QTreeView example")
tree.resize(420, 260)
tree.show()
sys.exit(app.exec())
- Each row is a list of items, one per column:
appendRow([name_item, type_item]). - Appending to an item (not the model) creates a child row.
expandAll()opens every level;expand(index)opens one.
QTreeView vs QTreeWidget
QTreeWidget is a convenience class with its own built-in model: you create QTreeWidgetItems directly. QTreeView works with any model, so the same data can be shown, sorted and filtered in several views:
import sys
from PyQt6.QtWidgets import QApplication, QTreeWidget, QTreeWidgetItem
app = QApplication(sys.argv)
tree = QTreeWidget()
tree.setHeaderLabels(["Name", "Type"])
fruits = QTreeWidgetItem(tree, ["Fruits", "Category"]) # items know their parent
QTreeWidgetItem(fruits, ["Apple", "Pome"])
QTreeWidgetItem(fruits, ["Mango", "Drupe"])
vegetables = QTreeWidgetItem(tree, ["Vegetables", "Category"])
QTreeWidgetItem(vegetables, ["Carrot", "Root"])
tree.expandAll()
tree.setColumnWidth(0, 200)
tree.setWindowTitle("QTreeWidget example")
tree.resize(420, 260)
tree.show()
sys.exit(app.exec())
QTreeWidget | QTreeView + model | |
|---|---|---|
| Setup | Least code | A model plus a view |
| Filtering, sorting proxies | Limited | Yes (QSortFilterProxyModel) |
| File system, databases, big data | No | Yes (QFileSystemModel, custom models) |
| Same data in several views | No | Yes |
| Best for | Small, fixed trees | Everything else |
Build a tree from a nested dict
Real data is usually nested dictionaries and lists (JSON, API responses). A small recursive function turns any depth into tree items:
import sys
from PyQt6.QtGui import QStandardItem, QStandardItemModel
from PyQt6.QtWidgets import QApplication, QTreeView
data = {
"USA": {"California": ["Los Angeles", "San Francisco"], "Texas": ["Austin", "Houston"]},
"India": {"Maharashtra": ["Mumbai", "Pune"], "Karnataka": ["Bengaluru"]},
"UK": ["London", "Manchester"],
}
def add_items(parent, value):
"""Add a dict, list or single value under parent (recursively)."""
if isinstance(value, dict):
for key, child in value.items():
item = QStandardItem(str(key))
parent.appendRow(item)
add_items(item, child)
elif isinstance(value, list):
for child in value:
add_items(parent, child)
else:
parent.appendRow(QStandardItem(str(value)))
app = QApplication(sys.argv)
model = QStandardItemModel()
model.setHorizontalHeaderLabels(["Places"])
add_items(model.invisibleRootItem(), data)
tree = QTreeView()
tree.setModel(model)
tree.expandAll()
tree.setWindowTitle("Tree from a nested dict")
tree.resize(320, 380)
tree.show()
sys.exit(app.exec())
File browser with QFileSystemModel
QFileSystemModel reads folders from disk (loading them in the background as they’re expanded) and shows name, size, type and date columns. setRootIndex() makes the tree start at your folder:
import sys
from pathlib import Path
from PyQt6.QtGui import QFileSystemModel
from PyQt6.QtWidgets import QApplication, QTreeView
app = QApplication(sys.argv)
folder = str(Path("my_project").resolve()) # the folder to show
model = QFileSystemModel()
model.setRootPath(folder)
tree = QTreeView()
tree.setModel(model)
tree.setRootIndex(model.index(folder)) # start at that folder, not at the drive
tree.setColumnWidth(0, 220)
tree.setSortingEnabled(True)
tree.setWindowTitle("File browser with QFileSystemModel")
tree.resize(620, 330)
tree.show()
sys.exit(app.exec())
Use model.setNameFilters(["*.py"]) and model.setNameFilterDisables(False) to show only matching files, and tree.hideColumn(1) to hide a column.
Get the selected item and its path
clicked fires on a mouse click; selectionModel().currentChanged fires whenever the current item changes, including from the keyboard. Walk up with index.parent() to build the full path:
import sys
from PyQt6.QtGui import QStandardItem, QStandardItemModel
from PyQt6.QtWidgets import QApplication, QTreeView
app = QApplication(sys.argv)
model = QStandardItemModel()
usa = QStandardItem("USA")
texas = QStandardItem("Texas")
usa.appendRow(texas)
texas.appendRow(QStandardItem("Austin"))
model.appendRow(usa)
tree = QTreeView()
tree.setModel(model)
def item_path(index):
"""Walk up the parents: USA > Texas > Austin."""
parts = []
while index.isValid():
parts.insert(0, index.data())
index = index.parent()
return " > ".join(parts)
def on_current_changed(current, previous):
print("selected:", current.data(), "| path:", item_path(current), "| depth:", len(item_path(current).split(" > ")) - 1)
tree.selectionModel().currentChanged.connect(on_current_changed)
tree.clicked.connect(lambda index: print("clicked:", index.data()))
# select Austin from code (a mouse click does the same)
austin = model.index(0, 0, model.index(0, 0, model.index(0, 0)))
tree.setCurrentIndex(austin)
tree.clicked.emit(austin)
Output (Austin selected):
selected: Austin | path: USA > Texas > Austin | depth: 2
clicked: Austin
Checkboxes with an automatic parent state
Make items checkable with setCheckable(True). You might expect setAutoTristate(True) to keep parents in sync, but in QStandardItemModel it doesn’t: in this test both children are checked and the parent stays unchecked:
import sys
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QStandardItem, QStandardItemModel
from PyQt6.QtWidgets import QApplication
app = QApplication(sys.argv)
model = QStandardItemModel()
parent = QStandardItem("Cheese")
parent.setCheckable(True)
parent.setAutoTristate(True)
for name in ["Mozzarella", "Cheddar"]:
child = QStandardItem(name)
child.setCheckable(True)
parent.appendRow(child)
model.appendRow(parent)
parent.child(0).setCheckState(Qt.CheckState.Checked)
parent.child(1).setCheckState(Qt.CheckState.Checked)
print("both children checked, parent is:", parent.checkState().name)
Output:
both children checked, parent is: Unchecked
The reliable way is a small itemChanged handler: clicking a parent sets all its children, and changing a child recomputes the parent (checked, unchecked or partly checked). The updating flag stops the handler from reacting to its own changes:
import sys
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QStandardItem, QStandardItemModel
from PyQt6.QtWidgets import QApplication, QTreeView
app = QApplication(sys.argv)
model = QStandardItemModel()
model.setHorizontalHeaderLabels(["Toppings"])
for group, toppings in {"Vegetables": ["Onion", "Pepper", "Olive"], "Cheese": ["Mozzarella", "Cheddar"]}.items():
parent = QStandardItem(group)
parent.setCheckable(True)
for name in toppings:
child = QStandardItem(name)
child.setCheckable(True)
parent.appendRow(child)
model.appendRow(parent)
updating = False
def on_item_changed(item):
# keep parents and children in sync when a checkbox changes
global updating
if updating or not item.isCheckable():
return
updating = True
state = item.checkState()
if item.hasChildren() and state != Qt.CheckState.PartiallyChecked:
for r in range(item.rowCount()): # a parent was clicked: apply to its children
item.child(r).setCheckState(state)
parent = item.parent()
if parent is not None: # a child changed: recompute its parent
states = {parent.child(r).checkState() for r in range(parent.rowCount())}
parent.setCheckState(states.pop() if len(states) == 1 else Qt.CheckState.PartiallyChecked)
updating = False
model.itemChanged.connect(on_item_changed)
def checked_items():
result = []
for r in range(model.rowCount()):
parent = model.item(r)
for c in range(parent.rowCount()):
if parent.child(c).checkState() == Qt.CheckState.Checked:
result.append(parent.child(c).text())
return result
tree = QTreeView()
tree.setModel(model)
tree.expandAll()
tree.setWindowTitle("Checkable tree")
tree.resize(300, 300)
tree.show()
sys.exit(app.exec())
Reading the result after ticking Onion, Mozzarella and Cheddar:
checked: ['Onion', 'Mozzarella', 'Cheddar']
Vegetables: PartiallyChecked | Cheese: Checked
If you use QTreeWidget, this is built in: set the ItemIsAutoTristate flag on the parent:
import sys
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QTreeWidget, QTreeWidgetItem
app = QApplication(sys.argv)
tree = QTreeWidget()
tree.setHeaderLabels(["Toppings"])
cheese = QTreeWidgetItem(tree, ["Cheese"])
cheese.setFlags(cheese.flags() | Qt.ItemFlag.ItemIsAutoTristate) # built into QTreeWidget
kids = []
for name in ["Mozzarella", "Cheddar"]:
kid = QTreeWidgetItem(cheese, [name])
kid.setFlags(kid.flags() | Qt.ItemFlag.ItemIsUserCheckable)
kid.setCheckState(0, Qt.CheckState.Unchecked)
kids.append(kid)
kids[0].setCheckState(0, Qt.CheckState.Checked)
print("one child checked ->", cheese.checkState(0).name)
kids[1].setCheckState(0, Qt.CheckState.Checked)
print("both checked ->", cheese.checkState(0).name)
cheese.setCheckState(0, Qt.CheckState.Unchecked)
print("parent unchecked ->", [k.checkState(0).name for k in kids])
Output:
one child checked -> PartiallyChecked
both checked -> Checked
parent unchecked -> ['Unchecked', 'Unchecked']
Search and filter the tree
Put a QSortFilterProxyModel between the model and the view. setRecursiveFilteringEnabled(True) keeps the parent rows of matching children visible:
import sys
from PyQt6.QtCore import QSortFilterProxyModel, Qt
from PyQt6.QtGui import QStandardItem, QStandardItemModel
from PyQt6.QtWidgets import QApplication, QLineEdit, QTreeView, QVBoxLayout, QWidget
data = {"Fruits": ["Apple", "Apricot", "Banana", "Mango"],
"Vegetables": ["Asparagus", "Carrot", "Potato"],
"Nuts": ["Almond", "Cashew", "Walnut"]}
app = QApplication(sys.argv)
model = QStandardItemModel()
model.setHorizontalHeaderLabels(["Food"])
for group, items in data.items():
parent = QStandardItem(group)
for name in items:
parent.appendRow(QStandardItem(name))
model.appendRow(parent)
proxy = QSortFilterProxyModel()
proxy.setSourceModel(model)
proxy.setRecursiveFilteringEnabled(True) # keep the parents of matching children
proxy.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
search = QLineEdit(placeholderText="Search...")
tree = QTreeView()
tree.setModel(proxy)
def on_search(text):
proxy.setFilterFixedString(text)
tree.expandAll()
search.textChanged.connect(on_search)
window = QWidget()
layout = QVBoxLayout(window)
layout.addWidget(search)
layout.addWidget(tree)
window.setWindowTitle("Search a QTreeView")
window.resize(320, 330)
window.show()
tree.expandAll()
sys.exit(app.exec())
Add, rename and delete items (context menu)
A right-click menu gives users control. indexAt() finds the item under the mouse; QStandardItems are editable by default, so tree.edit(index) starts renaming:
import sys
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QStandardItem, QStandardItemModel
from PyQt6.QtWidgets import QApplication, QMenu, QTreeView
app = QApplication(sys.argv)
model = QStandardItemModel()
model.setHorizontalHeaderLabels(["Tasks"])
project = QStandardItem("Website")
for task in ["Design", "Build"]:
project.appendRow(QStandardItem(task))
model.appendRow(project)
tree = QTreeView()
tree.setModel(model)
tree.expandAll()
def add_child(index, text="New task"):
parent = model.itemFromIndex(index) if index.isValid() else model.invisibleRootItem()
parent.appendRow(QStandardItem(text))
tree.expand(index)
def delete_item(index):
model.removeRow(index.row(), index.parent())
def show_menu(pos):
index = tree.indexAt(pos)
menu = QMenu(tree)
menu.addAction("Add child", lambda: add_child(index))
if index.isValid():
menu.addAction("Rename", lambda: tree.edit(index)) # items are editable by default
menu.addAction("Delete", lambda: delete_item(index))
menu.exec(tree.viewport().mapToGlobal(pos))
tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
tree.customContextMenuRequested.connect(show_menu)
tree.setWindowTitle("Add, rename and delete items")
tree.resize(320, 260)
tree.show()
sys.exit(app.exec())
Output (tree after running Add child, Rename and Delete from code):
Website
Design v2
Test
Icons, sorting and styling
Standard icons come from the current style, setSortingEnabled(True) lets users sort by clicking a header, and a stylesheet changes colours and spacing:
import sys
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QStandardItem, QStandardItemModel
from PyQt6.QtWidgets import QApplication, QHeaderView, QStyle, QTreeView
app = QApplication(sys.argv)
folder_icon = app.style().standardIcon(QStyle.StandardPixmap.SP_DirIcon)
file_icon = app.style().standardIcon(QStyle.StandardPixmap.SP_FileIcon)
model = QStandardItemModel()
model.setHorizontalHeaderLabels(["Name", "Size", "Modified"])
for folder, files in {"Reports": [("q1.pdf", "220 KB", "2026-03-31"), ("q2.pdf", "235 KB", "2026-06-30")],
"Images": [("logo.png", "18 KB", "2026-01-12"), ("banner.jpg", "410 KB", "2026-02-02")]}.items():
parent = QStandardItem(folder_icon, folder)
for name, size, date in files:
size_item = QStandardItem(size)
size_item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
parent.appendRow([QStandardItem(file_icon, name), size_item, QStandardItem(date)])
model.appendRow([parent, QStandardItem(), QStandardItem()])
tree = QTreeView()
tree.setModel(model)
tree.setAlternatingRowColors(True)
tree.setSortingEnabled(True) # click a header to sort
tree.sortByColumn(0, Qt.SortOrder.AscendingOrder)
tree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
tree.setStyleSheet("""
QTreeView { font-size: 13px; }
QTreeView::item { padding: 4px; }
QTreeView::item:selected { background: #306998; color: white; }
QHeaderView::section { background: #0a1b2e; color: white; padding: 5px; border: none; }
""")
tree.expandAll()
tree.setWindowTitle("Icons, sorting and styling")
tree.resize(520, 300)
tree.show()
sys.exit(app.exec())
A custom model (QAbstractItemModel)
When your data already lives in Python objects, or is too large to copy into QStandardItems, write a model. A read-only tree model needs five methods: index, parent, rowCount, columnCount and data:
import sys
from PyQt6.QtCore import QAbstractItemModel, QModelIndex, Qt
from PyQt6.QtWidgets import QApplication, QTreeView
class Node:
def __init__(self, name, size="", parent=None):
self.name, self.size, self.parent, self.children = name, size, parent, []
if parent:
parent.children.append(self)
class TreeModel(QAbstractItemModel):
"""A read-only model over your own Python objects."""
def __init__(self, root):
super().__init__()
self.root = root
def index(self, row, column, parent=QModelIndex()):
if not self.hasIndex(row, column, parent):
return QModelIndex()
node = parent.internalPointer() if parent.isValid() else self.root
return self.createIndex(row, column, node.children[row])
def parent(self, index):
node = index.internalPointer() if index.isValid() else None
if node is None or node.parent is self.root or node.parent is None:
return QModelIndex()
grand = node.parent.parent
return self.createIndex(grand.children.index(node.parent), 0, node.parent)
def rowCount(self, parent=QModelIndex()):
if parent.column() > 0: # only column 0 has children
return 0
node = parent.internalPointer() if parent.isValid() else self.root
return len(node.children)
def columnCount(self, parent=QModelIndex()):
return 2
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
if index.isValid() and role == Qt.ItemDataRole.DisplayRole:
node = index.internalPointer()
return node.name if index.column() == 0 else node.size
return None
def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
return ["Name", "Size"][section]
return None
root = Node("root")
src = Node("src", parent=root)
Node("main.py", "4 KB", src)
Node("utils.py", "2 KB", src)
Node("README.md", "1 KB", root)
app = QApplication(sys.argv)
model = TreeModel(root)
tree = QTreeView()
tree.setModel(model)
tree.expandAll()
tree.setColumnWidth(0, 200)
tree.setWindowTitle("Custom QAbstractItemModel")
tree.resize(360, 220)
tree.show()
sys.exit(app.exec())
Two rules are easy to miss: only column 0 may have children (rowCount() returns 0 for other columns), and index() should check hasIndex() first. Qt ships a checker for models, QAbstractItemModelTester; my first draft of this model broke those rules and the checker caught it. The version above passes:
rows at top: 2 | children of src: 2
parent of main.py: src
Common problems
- Nothing shows: keep a reference to the model (e.g.
self.model); a model created in a function and not stored can be garbage-collected. - Only the top level appears: children are collapsed; call
expandAll()or click the arrows. - QFileSystemModel shows the whole drive: call
tree.setRootIndex(model.index(folder)). - Filter hides children’s parents: enable
setRecursiveFilteringEnabled(True)on the proxy. - Selected index from a proxy: convert with
proxy.mapToSource(index)before using it with the source model.
More PyQt6 tutorials:
Frequently asked questions
How do I create a QTreeView in PyQt6?
Create a QStandardItemModel, add QStandardItem rows (append items to other items for children), then call tree.setModel(model) on a QTreeView.
What is the difference between QTreeView and QTreeWidget?
QTreeWidget has a built-in model and is quickest for small fixed trees. QTreeView works with any model, which enables file system models, custom models, sorting and filtering proxies.
How do I get the selected item in a QTreeView?
Use tree.currentIndex() or connect to tree.selectionModel().currentChanged. index.data() gives the text; model.itemFromIndex(index) gives the QStandardItem.
How do I add checkboxes to a QTreeView?
Call item.setCheckable(True) on each item. To keep parents in sync with their children in a QStandardItemModel, connect model.itemChanged to a handler like the one above; in QTreeWidget, set the ItemIsAutoTristate flag on parents.
How do I filter a QTreeView?
Use a QSortFilterProxyModel between model and view, call setRecursiveFilteringEnabled(True), and set the text with setFilterFixedString().
Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn