Why Omitting "if role == Qt.DisplayRole" Makes Your QAbstractTableModel Look Strange

Understanding Qt roles and why your table suddenly shows checkboxes, checkmarks, and filled squares
Heads up! You've already completed this tutorial.

If you've been following a tutorial on QAbstractTableModel and accidentally left out the line if role == Qt.DisplayRole: from your data method, you might have been greeted by something unexpected: checkboxes, checkmarks, and mysterious filled squares scattered across your table.

This is a great accident to make, because understanding why it happens gives you a much deeper insight into how Qt's Model/View architecture works behind the scenes.

What happened?

Here's a typical data method that formats values for display in a table:

python
def data(self, index, role):
    if role == Qt.DisplayRole:  # <-- this line was accidentally omitted
        # Get the raw value
        value = self._data[index.row()][index.column()]

        # Perform per-type checks and render accordingly
        if isinstance(value, datetime.date):
            return value.strftime("%Y-%m-%d")
        if isinstance(value, float):
            return "%.2f" % value
        if isinstance(value, str):
            return '"%s"' % value
        # Default (anything not captured above: e.g. int)
        return value

When you remove that if role == Qt.DisplayRole: guard, the code still runs without raising an error. But the table output looks like this:

Strange table output with checkboxes and filled squares

Checkboxes everywhere, some checked, some unchecked, and some showing a filled square. Let's figure out why.

How the data method actually works

The data method on a model is called by the view — repeatedly. Every time the view needs to know something about a cell, it calls data and passes two arguments:

  • index — tells you where (which row and column)
  • role — tells you what the view is asking about

The role parameter is the piece that matters here. Qt doesn't just ask "what value should I display?" It also asks things like "what background color should this cell have?" or "should this cell show a checkbox?" Each of these questions is represented by a different role.

Some common roles include:

Role What the view is asking
Qt.DisplayRole "What text should I display?"
Qt.BackgroundRole "What background color should I use?"
Qt.ForegroundRole "What text color should I use?"
Qt.CheckStateRole "Should I show a checkbox, and what state should it be in?"
Qt.DecorationRole "Should I show an icon?"

When the view renders a single cell, it calls your data method multiple times — once for each role it cares about. Your method is expected to check which role is being asked about and return an appropriate answer, or return None to say "I have nothing for that role."

What happens when you skip the role check

Normally, you nest your return values under if role == Qt.DisplayRole: so that your formatted strings are only returned when Qt is asking for display text. For all other roles, your method implicitly returns None (Python functions return None by default when they reach the end without an explicit return), and the view treats that as "nothing to do here."

When you remove that guard, your method returns the same value regardless of which role is being asked about. Qt asks "should I show a checkbox?" and your method hands back an integer. Qt asks "what background color?" and your method hands back a date string. You're answering every question with the same answer, and Qt does its best to interpret whatever you give it.

Why checkboxes appear

The most visually obvious result is the checkboxes, and that comes down to Qt.CheckStateRole.

When Qt calls your data method with role == Qt.CheckStateRole, it expects to receive a Qt.CheckState value in return. If you return None, no checkbox is shown. But since you removed the role check, your method returns the cell's actual data value instead.

Qt.CheckState is an enum with three possible values:

Constant Integer value Meaning
Qt.Unchecked 0 Empty checkbox
Qt.PartiallyChecked 1 Checkbox with a filled square
Qt.Checked 2 Checkbox with a checkmark

These enum values are just integers underneath. Qt.Unchecked == 0 is True. So when your data method returns an integer like 2 for Qt.CheckStateRole, Qt interprets that as Qt.Checked and draws a ticked checkbox.

You can verify this yourself with a simple experiment:

python
def data(self, index, role):
    if role == Qt.CheckStateRole:
        return 2  # Every cell shows a checked checkbox

Or to see the "partially checked" square:

python
def data(self, index, role):
    if role == Qt.CheckStateRole:
        return 1  # Every cell shows a filled square

Mapping the screenshot back to the data

Looking at the screenshot again, you can now explain each checkbox state by looking at the integer value in that cell:

Strange table output with checkboxes and filled squares

  • Cells showing a checkmark (✓) — the underlying data value is 2, which Qt interprets as Qt.Checked.
  • Cells showing a filled square (■) — the underlying data value is 1, which Qt interprets as Qt.PartiallyChecked.
  • Cells showing an empty checkbox — the data value is something other than 0, 1, or 2, but it's not None either. Returning a non-None value for Qt.CheckStateRole tells Qt "yes, show a checkbox," but since the value doesn't map to a recognized check state, it defaults to unchecked.

Cells that return strings, floats, or date objects for Qt.CheckStateRole still cause a checkbox to appear — any non-None return value is enough to trigger that — but the checkbox state falls back to unchecked because the value can't be interpreted as a valid Qt.CheckState.

The fix

The fix is straightforward: always check the role before returning data.

python
def data(self, index, role):
    if role == Qt.DisplayRole:
        value = self._data[index.row()][index.column()]

        if isinstance(value, datetime.date):
            return value.strftime("%Y-%m-%d")
        if isinstance(value, float):
            return "%.2f" % value
        if isinstance(value, str):
            return '"%s"' % value
        return value

By wrapping everything inside if role == Qt.DisplayRole:, you ensure that your formatted values are only returned when Qt is asking for display text. For every other role — Qt.CheckStateRole, Qt.BackgroundRole, and so on — your method returns None, which tells Qt "I have no special instructions for that."

Lessons from this happy accident

This kind of mistake is actually a great learning opportunity. It reveals several things about how Qt's Model/View system works:

  1. The data method is called multiple times per cell, once for each role the view is interested in. Your method needs to handle (or ignore) each role appropriately.

  2. Roles are just integers, and so are many Qt enums. When you return an integer for a role that expects an enum, Qt will happily interpret it according to that enum's values. This is how a data value of 2 accidentally becomes a checked checkbox.

  3. Returning None is meaningful. It tells the view "I have nothing to say about this." That's why the default behavior of Python functions (returning None when no explicit return is hit) works perfectly as a "skip this role" mechanism.

  4. Always guard your return values with role checks. As you add more roles to your data method (for colors, tooltips, alignment, and so on), keeping each block under its own if role == ...: check ensures that your answers only go to the right questions.

Understanding this pattern makes it much easier to extend your models later. Want to add background colors? Add a block for Qt.BackgroundRole. Want tooltips? Add one for Qt.ToolTipRole. Each role is a separate conversation between the view and your model, and your data method is where you manage all those conversations. To see these concepts applied with real data using numpy and pandas, take a look at the QTableView with numpy and pandas tutorial. You can also learn more about signals, slots and events which are another core part of how Qt views communicate with your application.

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

PyQt/PySide Office Hours 1:1 with Martin Fitzpatrick

Save yourself time and frustration. Get one on one help with your projects. Bring issues, bugs and questions about usability to architecture and maintainability, and leave with solutions.

Book Now 60 mins ($195)

Martin Fitzpatrick

Why Omitting "if role == Qt.DisplayRole" Makes Your QAbstractTableModel Look Strange was written by Martin Fitzpatrick.

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt. Martin founded PythonGUIs to provide easy to follow GUI programming tutorials to the Python community. He has written a number of popular Python books on the subject.