When working with data in Python, you'll often have it stored as a list of dictionaries — each dictionary representing a row of data, with keys as column names. This is a very common format, especially when loading data from APIs, databases, or JSON files. But Qt's table models work with numeric row and column indexes, so how do you bridge the gap?
In this tutorial, we'll build a custom QAbstractTableModel that takes a list of dictionaries and displays it in a QTableView. You don't need pandas or numpy — just plain Python and PyQt6.
How Qt Table Models Work
Qt's model/view architecture expects your data model to respond to requests like "give me the value at row 2, column 3." These requests arrive as QModelIndex objects with .row() and .column() methods that return integers.
That works perfectly for a list of lists — you just index twice, like data[row][column]. But for a list of dictionaries, columns are identified by keys, not numbers. So we need a way to map column numbers to dictionary keys.
The solution is simple: maintain a list of keys (which become your column headers) and use the column index to look up the right key.
Building the Model
Let's start with the model class. We subclass QAbstractTableModel and implement the required methods: data(), rowCount(), and columnCount(). We'll also implement headerData() so our table shows meaningful column headers.
from PyQt6.QtCore import QAbstractTableModel, Qt
class DictionaryTableModel(QAbstractTableModel):
def __init__(self, data, headers):
super().__init__()
self._data = data
self._headers = headers
def data(self, index, role):
if role == Qt.DisplayRole:
column_key = self._headers[index.column()]
return self._data[index.row()][column_key]
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._headers)
def headerData(self, section, orientation, role):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return str(self._headers[section])
if orientation == Qt.Vertical:
return str(section)
The _headers list serves double duty here: it defines the order of columns and provides the dictionary keys used to look up values. In the data() method, we convert the column index to a key using self._headers[index.column()], then use that key to get the value from the dictionary at the corresponding row.
Putting It Together
Now let's wire this model up to a QTableView inside a QMainWindow:
import sys
from PyQt6 import QtWidgets
from PyQt6.QtCore import QAbstractTableModel, Qt
class DictionaryTableModel(QAbstractTableModel):
def __init__(self, data, headers):
super().__init__()
self._data = data
self._headers = headers
def data(self, index, role):
if role == Qt.DisplayRole:
column_key = self._headers[index.column()]
return self._data[index.row()][column_key]
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._headers)
def headerData(self, section, orientation, role):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return str(self._headers[section])
if orientation == Qt.Vertical:
return str(section)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.table = QtWidgets.QTableView()
data = [
{"a": 4, "b": 9, "c": 2},
{"a": 1, "b": 0, "c": 0},
{"a": 3, "b": 5, "c": 0},
{"a": 3, "b": 3, "c": 2},
{"a": 7, "b": 8, "c": 9},
]
headers = ["a", "b", "c"]
self.model = DictionaryTableModel(data, headers)
self.table.setModel(self.model)
self.setCentralWidget(self.table)
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Run this and you'll see a table with three columns labeled "a", "b", and "c", filled with the values from each dictionary.
Using Different Display Headers
You might want your column headers to show something more readable than the raw dictionary keys. For example, your data might use keys like "first_name" but you'd rather display "First Name" in the table header.
You can handle this by passing a separate list of display names and keeping the keys for data lookup:
import sys
from PyQt6 import QtWidgets
from PyQt6.QtCore import QAbstractTableModel, Qt
class DictionaryTableModel(QAbstractTableModel):
def __init__(self, data, keys, display_headers=None):
super().__init__()
self._data = data
self._keys = keys
self._display_headers = display_headers or keys
def data(self, index, role):
if role == Qt.DisplayRole:
column_key = self._keys[index.column()]
return self._data[index.row()][column_key]
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._keys)
def headerData(self, section, orientation, role):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return str(self._display_headers[section])
if orientation == Qt.Vertical:
return str(section)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.table = QtWidgets.QTableView()
data = [
{"first_name": "Alice", "last_name": "Smith", "age": 30},
{"first_name": "Bob", "last_name": "Jones", "age": 25},
{"first_name": "Carol", "last_name": "Davis", "age": 42},
]
keys = ["first_name", "last_name", "age"]
display_headers = ["First Name", "Last Name", "Age"]
self.model = DictionaryTableModel(data, keys, display_headers)
self.table.setModel(self.model)
self.setCentralWidget(self.table)
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Now the table displays "First Name", "Last Name", and "Age" as column headers, while the model still uses the original dictionary keys internally.
Generating Headers Automatically
If your dictionaries don't all have the same keys, or you just want to avoid specifying headers manually, you can generate them from the data itself. One approach is to collect all unique keys across every dictionary:
def generate_headers(data):
keys = set()
for row in data:
keys.update(row.keys())
return sorted(keys)
Using sorted() gives you a consistent column order. You can use this function to build the headers list before creating the model:
data = [
{"a": 4, "b": 9, "c": 2},
{"a": 1, "c": 5}, # missing "b"
{"a": 3, "b": 3, "d": 7}, # has extra "d"
]
headers = generate_headers(data)
# headers = ['a', 'b', 'c', 'd']
Handling Missing Keys
When your dictionaries don't all have the same keys, looking up a missing key will raise a KeyError. You can handle this gracefully in the data() method using .get():
def data(self, index, role):
if role == Qt.DisplayRole:
column_key = self._headers[index.column()]
value = self._data[index.row()].get(column_key, "")
return str(value)
Using dict.get(key, default) returns the default value (here, an empty string) when the key doesn't exist, instead of raising an error. This makes the model robust when dealing with inconsistent data.
Complete Working Example
Here's a full example that brings everything together — automatic header generation, display-friendly headers, and safe handling of missing keys:
import sys
from PyQt6 import QtWidgets
from PyQt6.QtCore import QAbstractTableModel, Qt
def generate_headers(data):
"""Collect all unique keys from a list of dictionaries."""
keys = set()
for row in data:
keys.update(row.keys())
return sorted(keys)
class DictionaryTableModel(QAbstractTableModel):
def __init__(self, data, headers=None):
super().__init__()
self._data = data
self._headers = headers or generate_headers(data)
def data(self, index, role):
if role == Qt.DisplayRole:
column_key = self._headers[index.column()]
value = self._data[index.row()].get(column_key, "")
return str(value)
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._headers)
def headerData(self, section, orientation, role):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return str(self._headers[section])
if orientation == Qt.Vertical:
return str(section)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("List of Dict Table")
self.table = QtWidgets.QTableView()
data = [
{"name": "Alice", "department": "Engineering", "salary": 95000},
{"name": "Bob", "department": "Marketing", "salary": 72000},
{"name": "Carol", "department": "Engineering", "role": "Lead"},
{"name": "Dave", "salary": 68000, "role": "Intern"},
]
self.model = DictionaryTableModel(data)
self.table.setModel(self.model)
self.setCentralWidget(self.table)
self.resize(600, 300)
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Notice that the data has inconsistent keys — "Carol" has no "salary" and "Dave" has no "department." The model handles this gracefully, showing empty cells where data is missing.
This approach keeps things simple and Pythonic. You get a fully functional table view from a list of dictionaries, with no need for pandas or numpy. From here, you could extend the model to support editing, sorting and filtering, or custom formatting — but the core pattern of mapping column indexes to dictionary keys stays the same. If you're working with numpy arrays or pandas DataFrames instead, take a look at our QTableView with numpy and pandas tutorial.
Packaging Python Applications with PyInstaller by Martin Fitzpatrick
This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.