Display Data with Different Column Sizes in a QAbstractTableModel

How to handle jagged or uneven row lengths when displaying file data in a QTableView
Heads up! You've already completed this tutorial.

I'm loading data from a text file where each row can have a different number of columns. How do I display this kind of jagged data in a QTableView using a custom QAbstractTableModel?

When you load data from a text file, each line might have a different number of values. For example, one row might have 3 columns and another might have 5. A QTableView expects a uniform grid — every row should have the same number of columns. So what happens when some rows are shorter than others?

Qt will still ask your model for data in every cell of the grid, including cells that don't exist in your shorter rows. If your Python code tries to access a list index that doesn't exist, you'll get an IndexError and things won't work as expected.

In this article, we'll walk through why this happens and look at two clean ways to handle it.

Why the model expects uniform data

When you subclass QAbstractTableModel, you provide a rowCount() and a columnCount(). These values define the size of the grid that the QTableView will render. The column count applies to every row — there's no way to tell Qt that row 0 has 3 columns but row 2 has 5.

If you set columnCount to the maximum number of columns found in any row, Qt will call your data() method for every cell in that full grid. For shorter rows, the index it passes will point beyond the end of the list for that row.

Here's the problem in action. Say your data looks like this after loading from a file:

python
data = [
    ["a", "b", "c", "d", "e"],
    ["f", "g"],
    ["h", "i", "j"],
]

The maximum column count is 5. When Qt asks for the value at row 1, column 3, your code tries data[1][3] — but row 1 only has 2 elements. Python raises an IndexError.

You might think you can check with something like if not data[row][col], but that line still accesses the index first. The not only evaluates after the value is retrieved, so the error is already thrown.

Approach 1: Catch the IndexError in data()

The simplest approach is to handle the missing data right where it's requested. In your data() method, wrap the list access in a try/except block. If the index is out of range, return an empty string (or None).

python
def data(self, index, role):
    if role == Qt.ItemDataRole.DisplayRole:
        try:
            value = self._data[index.row()][index.column()]
        except IndexError:
            return ""  # This cell doesn't exist in the data, return empty.

        if isinstance(value, float):
            return "%.2f" % value

        return value

This way, you never need to modify the original data. The model just gracefully returns an empty value for any cell that falls outside a shorter row.

Approach 2: Pad the data before passing it to the model

An alternative is to normalize the data so every row has the same number of columns. You do this by padding shorter rows with empty strings before creating the model.

python
max_columns = max(len(row) for row in data)

for row in data:
    while len(row) < max_columns:
        row.append("")

After this, every row has the same length, and the model can access any cell without issues. This is a perfectly valid approach — it just means you're modifying (or copying) the data before display.

Complete working example

Here's a full application that loads a text file, handles rows of different lengths using the try/except approach, and displays the result in a QTableView. If you're new to building PyQt6 applications, you may want to start with the PyQt6 first window tutorial before diving in.

python
import sys

from PyQt6 import QtCore, QtWidgets
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
    QApplication,
    QFileDialog,
    QPushButton,
    QVBoxLayout,
    QWidget,
)


class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data
        self._max_columns = max(len(row) for row in self._data) if self._data else 0

    def data(self, index, role):
        if role == Qt.ItemDataRole.TextAlignmentRole:
            return Qt.AlignmentFlag.AlignCenter

        if role == Qt.ItemDataRole.DisplayRole:
            try:
                value = self._data[index.row()][index.column()]
            except IndexError:
                return ""  # Cell doesn't exist in this row.

            if isinstance(value, float):
                return "%.2f" % value

            return value

    def rowCount(self, index):
        return len(self._data)

    def columnCount(self, index):
        return self._max_columns


class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Jagged Data Viewer")

        self.button = QPushButton("Load text file")
        self.button.clicked.connect(self.load_text_file)

        self.table = QtWidgets.QTableView()

        layout = QVBoxLayout()
        layout.addWidget(self.table)
        layout.addWidget(self.button)
        self.setLayout(layout)

    def load_text_file(self):
        file_name, _ = QFileDialog.getOpenFileName(
            self, "Open Text File", "", "Text Files (*.txt)"
        )
        if not file_name:
            return

        with open(file_name, "r") as f:
            lines = f.readlines()

        data = [line.split() for line in lines if line.strip()]

        self.model = TableModel(data)
        self.table.setModel(self.model)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

To test this, create a text file with rows of different lengths, for example:

python
apple banana cherry date elderberry
fig grape
hazelnut ice jujube
kiwi
lemon mango nectarine orange plum

Load this file using the button, and you'll see the data displayed in the table with empty cells where shorter rows don't have values.

Customizing header styles

Once you have the model working, you might want to customize how headers look — for example, changing colors or formatting the labels. You can do this by implementing the headerData() method on your model.

The headerData() method receives three arguments:

  • section — the index of the row or column (an integer).
  • orientation — either Qt.Orientation.Horizontal (column headers along the top) or Qt.Orientation.Vertical (row headers down the left side).
  • role — the same role system used in data(), such as DisplayRole, ForegroundRole, or BackgroundRole.

Here's a model with custom header data:

python
from PyQt6 import QtGui


class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

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

    def headerData(self, section, orientation, role):
        if orientation == Qt.Orientation.Horizontal:
            if role == Qt.ItemDataRole.DisplayRole:
                return f"Col {section}"

            if role == Qt.ItemDataRole.ForegroundRole and section == 1:
                return QtGui.QColor("red")

            if role == Qt.ItemDataRole.BackgroundRole and section == 0:
                return QtGui.QColor("lightblue")

        if orientation == Qt.Orientation.Vertical:
            if role == Qt.ItemDataRole.DisplayRole:
                return f"Row {section}"

    def rowCount(self, index):
        return len(self._data)

    def columnCount(self, index):
        return len(self._data[0])

One thing to be aware of: on some platforms (particularly Windows with the native style), BackgroundRole on headers may have no visible effect. If you need consistent header background colors across platforms, you can set a cross-platform style like Fusion:

python
app = QApplication(sys.argv)
app.setStyle("fusion")

This ensures your background colors appear as expected everywhere, though your application will use Fusion's look instead of the native platform style.

Putting it all together

Here's a complete example that combines everything — loading jagged data from a file, handling missing cells, and displaying custom headers:

python
import sys

from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
    QApplication,
    QFileDialog,
    QPushButton,
    QVBoxLayout,
    QWidget,
)


class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data
        self._max_columns = max(len(row) for row in self._data) if self._data else 0

    def data(self, index, role):
        if role == Qt.ItemDataRole.TextAlignmentRole:
            return Qt.AlignmentFlag.AlignCenter

        if role == Qt.ItemDataRole.DisplayRole:
            try:
                value = self._data[index.row()][index.column()]
            except IndexError:
                return ""

            if isinstance(value, float):
                return "%.2f" % value

            return value

        if role == Qt.ItemDataRole.BackgroundRole:
            # Shade missing cells with a light gray background.
            if index.column() >= len(self._data[index.row()]):
                return QtGui.QColor("#f0f0f0")

    def headerData(self, section, orientation, role):
        if orientation == Qt.Orientation.Horizontal:
            if role == Qt.ItemDataRole.DisplayRole:
                return f"Column {section + 1}"

        if orientation == Qt.Orientation.Vertical:
            if role == Qt.ItemDataRole.DisplayRole:
                return f"Row {section + 1}"

    def rowCount(self, index):
        return len(self._data)

    def columnCount(self, index):
        return self._max_columns


class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Jagged Data Viewer")
        self.resize(600, 400)

        self.button = QPushButton("Load text file")
        self.button.clicked.connect(self.load_text_file)

        self.table = QtWidgets.QTableView()

        layout = QVBoxLayout()
        layout.addWidget(self.table)
        layout.addWidget(self.button)
        self.setLayout(layout)

    def load_text_file(self):
        file_name, _ = QFileDialog.getOpenFileName(
            self, "Open Text File", "", "Text Files (*.txt)"
        )
        if not file_name:
            return

        with open(file_name, "r") as f:
            lines = f.readlines()

        data = [line.split() for line in lines if line.strip()]

        if data:
            self.model = TableModel(data)
            self.table.setModel(self.model)


app = QApplication(sys.argv)
app.setStyle("fusion")
window = MainWindow()
window.show()
sys.exit(app.exec())

In this version, missing cells are not only returned as empty strings — they're also shaded light gray using BackgroundRole, making it visually clear where data is absent. This is a nice touch when working with real-world files that often have inconsistent formatting.

The try/except pattern in data() is the recommended way to handle this situation. It keeps your original data intact, avoids unnecessary copying or padding, and lets you add visual indicators for missing cells whenever you need them.

For more on working with table models in PyQt6 — including displaying numpy arrays and pandas DataFrames — see the QTableView with numpy and pandas tutorial. If you want to understand the broader Model/View architecture that underpins QAbstractTableModel, take a look at the PyQt6 ModelView architecture guide. You can also learn how to sort and filter table data once your model is set up.

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

PyQt/PySide 1:1 Coaching with Martin Fitzpatrick

Save yourself time and frustration. Get one on one help with your Python GUI projects. Working together with you I'll identify issues and suggest fixes, from bugs and usability to architecture and maintainability.

Book Now 60 mins ($195)

Martin Fitzpatrick

Display Data with Different Column Sizes in a QAbstractTableModel 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.