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

« back to Python PyQt All tutorials