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
QTableViewusing a customQAbstractTableModel?
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:
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).
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.
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.
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:
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— eitherQt.Orientation.Horizontal(column headers along the top) orQt.Orientation.Vertical(row headers down the left side).role— the same role system used indata(), such asDisplayRole,ForegroundRole, orBackgroundRole.
Here's a model with custom header data:
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:
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:
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.
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.