I'm confused about the
data()method that overridesQAbstractListModel. It never seems to actually be invoked, despite being explained as the only way views can retrieve data from the model. How does the view actually get data from the model, and how are roles assigned to the data values?
This is a really common source of confusion when you're getting started with Qt's ModelView architecture. The data() method is being called — but not by your code. It's called automatically by the view. Let's walk through how this works and why it can feel invisible.
How ModelView works
In Qt's ModelView architecture, you have two main players:
- The model — holds and manages your data.
- The view — displays the data to the user.
The view never accesses your underlying data (like a Python list) directly. Instead, it asks the model for data by calling the model's data() method. This happens behind the scenes — Qt handles the communication for you. You never need to call data() yourself.
Here's a simple example to make this concrete. We'll create a custom model backed by a plain Python list, and connect it to a QListView:
import sys
from PyQt6.QtCore import QAbstractListModel, Qt
from PyQt6.QtWidgets import QApplication, QListView
class TodoModel(QAbstractListModel):
def __init__(self, todos=None):
super().__init__()
self.todos = todos or []
def rowCount(self, parent=None):
return len(self.todos)
def data(self, index, role):
if role == Qt.ItemDataRole.DisplayRole:
return self.todos[index.row()]
app = QApplication(sys.argv)
model = TodoModel(todos=["Buy milk", "Clean house", "Walk the dog"])
view = QListView()
view.setModel(model)
view.show()
sys.exit(app.exec())
When you run this, you'll see a list with three items. You didn't call data() anywhere — but it was called, many times, by the QListView.
The view calls data(), not you
When you call view.setModel(model), the view takes a reference to your model. From that point on, whenever the view needs to draw itself — for example, when the window first appears, or when the user scrolls — it calls your model's data() method for each visible item.
The view essentially says: "Model, what should I display for row 0? What about row 1?" and so on. Each of those questions is a call to data().
You can verify this by adding a print() inside your data() method:
def data(self, index, role):
print(f"data() called for row {index.row()}, role {role}")
if role == Qt.ItemDataRole.DisplayRole:
return self.todos[index.row()]
Run the app again and you'll see data() being called multiple times in the console output. The view is doing this automatically.
What are roles?
You'll notice that data() receives two arguments: an index (which tells you which item the view is asking about) and a role (which tells you what kind of data the view wants).
A single item in your model can provide different data for different purposes. The role parameter is how the view specifies what it needs. Some common roles include:
| Role | Value | Purpose |
|---|---|---|
Qt.ItemDataRole.DisplayRole |
0 | The text to display |
Qt.ItemDataRole.DecorationRole |
1 | An icon or color to show beside the text |
Qt.ItemDataRole.ToolTipRole |
3 | Text shown when hovering over the item |
Qt.ItemDataRole.BackgroundRole |
8 | Background color for the item |
Qt.ItemDataRole.ForegroundRole |
9 | Text color for the item |
The view calls data() multiple times for the same item, each time with a different role. For DisplayRole it wants the text. For DecorationRole it wants an icon. For BackgroundRole it wants a color. Your data() method decides what to return for each role.
Here's an example that returns different data depending on the role:
import sys
from PyQt6.QtCore import QAbstractListModel, Qt
from PyQt6.QtGui import QColor
from PyQt6.QtWidgets import QApplication, QListView
class TodoModel(QAbstractListModel):
def __init__(self, todos=None):
super().__init__()
self.todos = todos or []
def rowCount(self, parent=None):
return len(self.todos)
def data(self, index, role):
if role == Qt.ItemDataRole.DisplayRole:
return self.todos[index.row()]
if role == Qt.ItemDataRole.ToolTipRole:
return f"This is task #{index.row() + 1}"
if role == Qt.ItemDataRole.BackgroundRole:
if index.row() % 2 == 0:
return QColor("#e6f7ff")
app = QApplication(sys.argv)
model = TodoModel(todos=["Buy milk", "Clean house", "Walk the dog"])
view = QListView()
view.setModel(model)
view.show()
sys.exit(app.exec())
In this version, even-numbered rows get a light blue background, and hovering over any item shows a tooltip. All of this is driven by the view calling data() with different roles.
Why you don't call data() yourself
In your own application code — for example, when adding or removing items — you work directly with the underlying data structure (the Python list). You don't need to go through data() to access your own data. The data() method exists as an interface for the view.
Think of it this way: the Python list is your internal storage, and data() is the public-facing window that the view looks through. You manage the list; the view reads from the model through data().
When you modify the underlying data, you do need to tell the model that something changed so the view knows to refresh. You do this by emitting signals. For example, after changing data, you can call self.layoutChanged.emit() to tell connected views to re-read everything:
import sys
from PyQt6.QtCore import QAbstractListModel, Qt
from PyQt6.QtGui import QColor
from PyQt6.QtWidgets import (
QApplication,
QListView,
QPushButton,
QVBoxLayout,
QWidget,
)
class TodoModel(QAbstractListModel):
def __init__(self, todos=None):
super().__init__()
self.todos = todos or []
def rowCount(self, parent=None):
return len(self.todos)
def data(self, index, role):
if role == Qt.ItemDataRole.DisplayRole:
return self.todos[index.row()]
if role == Qt.ItemDataRole.ToolTipRole:
return f"This is task #{index.row() + 1}"
if role == Qt.ItemDataRole.BackgroundRole:
if index.row() % 2 == 0:
return QColor("#e6f7ff")
def add_todo(self, text):
self.todos.append(text)
self.layoutChanged.emit()
app = QApplication(sys.argv)
model = TodoModel(todos=["Buy milk", "Clean house", "Walk the dog"])
window = QWidget()
layout = QVBoxLayout()
view = QListView()
view.setModel(model)
layout.addWidget(view)
counter = [0]
def add_item():
counter[0] += 1
model.add_todo(f"New task #{counter[0]}")
button = QPushButton("Add Task")
button.clicked.connect(add_item)
layout.addWidget(button)
window.setLayout(layout)
window.show()
sys.exit(app.exec())
Each time you click "Add Task", a new item is added to the internal list, layoutChanged is emitted, and the view automatically calls data() again for every visible row to refresh the display.
Putting it all together
Here's a summary of how the pieces fit:
- You subclass
QAbstractListModeland implementrowCount()anddata(). - You connect the model to a view using
view.setModel(model). - The view automatically calls
data()whenever it needs to display or update items. - The
roleparameter tells yourdata()method what kind of information the view is requesting. - You return the appropriate data for each role, or
Nonefor roles you don't handle. - When you change the underlying data, you emit a signal (like
layoutChanged) so the view knows to calldata()again.
The data() method is the bridge between your data and the view. You define it, and Qt calls it for you. That's why you never see an explicit call to data() in your own code — but it's working behind the scenes every time the view draws itself.
If you want to use this same approach with table data from numpy or pandas, take a look at how to display data in a QTableView using models. For building your own reusable widgets to use alongside model views, see the guide to creating custom widgets.
Create GUI Applications with Python & Qt6 by Martin Fitzpatrick
(PySide6 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!