PyQt Model View
Deep dive · part of Python PyQt
Qt's Model/View architecture separates data (model) from presentation (view). For tabular data, subclass QAbstractTableModel and implement rowCount, columnCount and data - the view handles scrolling, selection and rendering.
Model/View splits data (QAbstractTableModel) from presentation (QTableView, QListView). Views handle scrolling, selection, and headers; models implement rowCount, columnCount, data, and optional headerData—ideal for large tables without QWidget per cell.
Editing requires flags returning ItemIsEditable and setData implementing role-based updates. Sorting and filtering can proxy models (QSortFilterProxyModel).
Production code combines this topic with logging, tests, and clear module boundaries so refactors stay safe when requirements grow.
data(index, role) returns DisplayRole strings, UserRole arbitrary payloads.
beginResetModel/endResetModel wrap bulk changes notifying views.
QTableView.setModel attaches model; selection models track rows.
headerData supplies column titles for horizontal headers.
QModelIndex carries row/column/parent for tree models later.
DictModel pattern maps list of dicts to rectangular tables quickly.
Practice explaining pyqt model view aloud with a concrete example from your current project so the abstraction sticks beyond copy-paste exercises.
For thousands of rows, lazy fetch in data() beats loading all SQL into RAM. Delegate classes customize cell rendering (colors, icons) without subclassing the whole view.
Testing models without GUI: instantiate model, call data on QModelIndex created with model.index(row, col).
After setData, emit dataChanged for DisplayRole and EditRole so views refresh cells without full reset.
Unit-test models without QApplication by calling data() on QModelIndex from model.index().
Read the parent tutorial on pythondeck.com for runnable snippets, then reproduce them locally in a virtual environment with pinned dependency versions matching your deployment target.
When pairing with teammates, agree on one idiomatic pattern per concern—mixed styles in one repo slow reviews and invite subtle integration bugs during merges.
Emitting dataChanged for every cell on bulk reload—use reset model instead.
Returning QVariant inconsistently across roles.
Storing QWidget per row instead of using views—memory explosion.
Forgetting parent QObject ownership leading to segfaults on exit.
Implement minimal model methods first; add editing later.
Use UserRole to pass primary keys to selection handlers.
Profile SQL queries feeding models, not just Qt paint events.
Document column order matching headerData indices.
Re-read the examples below with these ideas in mind; change variable names and inputs to match your own project.
The program below demonstrates table model. Read the comments on each line, run the code, then change names or values to see how the output shifts.
# Example: Table model
# Run in the REPL or save as a .py file and execute with python.
from PyQt6.QtCore import QAbstractTableModel, Qt
from PyQt6.QtWidgets import QApplication, QTableView
class DictModel(QAbstractTableModel):
def __init__(self, rows, cols):
super().__init__()
self.rows, self.cols = rows, cols
def rowCount(self, _=None): return len(self.rows)
def columnCount(self, _=None): return len(self.cols)
def data(self, idx, role=Qt.ItemDataRole.DisplayRole):
if role == Qt.ItemDataRole.DisplayRole:
return str(self.rows[idx.row()][self.cols[idx.column()]])
def headerData(self, s, o, role=Qt.ItemDataRole.DisplayRole):
if role == Qt.ItemDataRole.DisplayRole and o == Qt.Orientation.Horizontal:
return self.cols[s]
app = QApplication([])
rows = [{"name":"Ada","score":99}, {"name":"Grace","score":97}]
view = QTableView()
view.setModel(DictModel(rows, ["name", "score"]))
view.show(); app.exec()
This sample walks through table model in a small, runnable script. Paste it into the REPL or save it as a .py file before you continue to the next block.
# QAbstractTableModel feeds data to QTableView
from PyQt6.QtCore import QAbstractTableModel, Qt # model API
from PyQt6.QtWidgets import QApplication, QTableView # view
class GridModel(QAbstractTableModel): # custom model
def __init__(self, rows): # rows list[dict]
super().__init__(); self.rows = rows; self.cols = list(rows[0]) # headers
def rowCount(self, _=None): return len(self.rows) # height
def columnCount(self, _=None): return len(self.cols) # width
def data(self, idx, role=Qt.ItemDataRole.DisplayRole): # cell text
if role == Qt.ItemDataRole.DisplayRole: # display role
return str(self.rows[idx.row()][self.cols[idx.column()]]) # value
app = QApplication([]) # need app
model = GridModel([{"name": "Ada", "score": 99}]) # data
view = QTableView(); view.setModel(model) # attach
print(model.rowCount(), model.columnCount()) # 1 x 2
Here is a hands-on illustration of header data. Follow the inline comments first; only then execute the snippet and compare the result with what you expected.
# headerData supplies column titles to the view
from PyQt6.QtCore import QAbstractTableModel, Qt # Qt core
class Simple(QAbstractTableModel): # one row model
def rowCount(self, _=None): return 1 # one row
def columnCount(self, _=None): return 2 # two cols
def data(self, idx, role=Qt.ItemDataRole.DisplayRole): # cells
return ["Ada", 99][idx.column()] if role == Qt.ItemDataRole.DisplayRole else None
def headerData(self, section, orient, role=Qt.ItemDataRole.DisplayRole): # headers
if orient == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
return ["name", "score"][section] # titles
m = Simple() # model
print(m.headerData(0, Qt.Orientation.Horizontal)) # name
Related deep dives on Python PyQt: