QTableWidget for List of Dict

Display Python dictionaries in a Qt table using QAbstractTableModel
Heads up! You've already completed this tutorial.

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.

python
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:

python
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:

python
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:

python
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:

python
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():

python
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:

python
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.

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

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.

More info Get the book

Martin Fitzpatrick

QTableWidget for List of Dict 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.