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.expa