Using Complex Data Sources with PyQt6 Model/View Architecture

How to use JSON, nested data, and other complex structures in your Qt table and list views
Heads up! You've already completed this tutorial.

I have a complex data set in a JSON file. Is it possible to use this as my model's data in PyQt views, or do I need to simplify it to a basic table?

Great news: you can use any data structure you like in a PyQt model. JSON files, nested dictionaries, lists of objects, database results — it all works. The model acts as an interface between how you store your data and how Qt expects to see it. As long as your model returns data in the format Qt expects, the view will display it without complaint.

In this tutorial, we'll walk through how to load a JSON file and display its data in a QTableView using a custom QAbstractTableModel. Along the way, you'll see how the model translates between your real-world data and Qt's row-and-column view of the world.

How Model/View Works with Complex Data

Qt's Model/View architecture separates data from presentation. The view (e.g. QTableView, QListView) asks the model questions like:

  • "How many rows are there?"
  • "How many columns are there?"
  • "What's the data at row 2, column 3?"

Your model answers these questions by looking at whatever underlying data structure you're using. The view doesn't care whether your data lives in a flat list, a JSON file, a pandas DataFrame, or a SQL database. It only cares about the answers.

This means you can wrap any data source in a model. You just need to translate between your data's shape and the row/column format that Qt uses.

A Simple JSON Example

Let's start with a straightforward example. Suppose you have a JSON file containing a list of people, where each person has several fields:

json
[
    {
        "name": "Alice",
        "age": 34,
        "city": "Berlin",
        "role": "Engineer"
    },
    {
        "name": "Bob",
        "age": 28,
        "city": "London",
        "role": "Designer"
    },
    {
        "name": "Charlie",
        "age": 41,
        "city": "New York",
        "role": "Manager"
    },
    {
        "name": "Diana",
        "age": 25,
        "city": "Tokyo",
        "role": "Developer"
    }
]

Save this as people.json in the same directory as your Python script.

Each object in the list becomes a row in the table, and each field becomes a column. The model's job is to map between these two representations.

Building the Model

To display this data in a QTableView, we subclass QAbstractTableModel and implement three required methods:

  • rowCount() — returns the number of rows
  • columnCount() — returns the number of columns
  • data() — returns the data for a given row and column

Here's how that looks:

python
import json
from PyQt6.QtCore import Qt, QAbstractTableModel


class JsonTableModel(QAbstractTableModel):
    def __init__(self, data, headers=None):
        super().__init__()
        self._data = data
        # Use provided headers, or extract keys from the first item.
        if headers:
            self._headers = headers
        elif data:
            self._headers = list(data[0].keys())
        else:
            self._headers = []

    def rowCount(self, parent=None):
        return len(self._data)

    def columnCount(self, parent=None):
        return len(self._headers)

    def data(self, index, role=Qt.ItemDataRole.DisplayRole):
        if role == Qt.ItemDataRole.DisplayRole:
            row = self._data[index.row()]
            key = self._headers[index.column()]
            return str(row.get(key, ""))
        return None

    def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
        if role == Qt.ItemDataRole.DisplayRole:
            if orientation == Qt.Orientation.Horizontal:
                return self._headers[section].capitalize()
        return None

Let's walk through what's happening here.

The __init__ method stores the list of dictionaries and figures out the column headers. If you don't provide headers explicitly, it grabs the keys from the first dictionary in the list.

The rowCount() method returns the length of the data list — one row per dictionary.

The columnCount() method returns the number of headers — one column per key.

The data() method is where the translation happens. Qt calls this method with an index (which contains a row and column number) and a role (which describes what kind of data Qt wants). For display purposes, we use Qt.ItemDataRole.DisplayRole. We look up the right dictionary from the list using the row number, then look up the right value using the column header as a key.

The headerData() method provides labels for the column headers at the top of the table.

Displaying the Data

Now let's wire everything together in a small application:

python
import sys
import json

from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView
from PyQt6.QtCore import Qt, QAbstractTableModel


class JsonTableModel(QAbstractTableModel):
    def __init__(self, data, headers=None):
        super().__init__()
        self._data = data
        if headers:
            self._headers = headers
        elif data:
            self._headers = list(data[0].keys())
        else:
            self._headers = []

    def rowCount(self, parent=None):
        return len(self._data)

    def columnCount(self, parent=None):
        return len(self._headers)

    def data(self, index, role=Qt.ItemDataRole.DisplayRole):
        if role == Qt.ItemDataRole.DisplayRole:
            row = self._data[index.row()]
            key = self._headers[index.column()]
            return str(row.get(key, ""))
        return None

    def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
        if role == Qt.ItemDataRole.DisplayRole:
            if orientation == Qt.Orientation.Horizontal:
                return self._headers[section].capitalize()
        return None


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("JSON Table Model")

        # Load data from the JSON file.
        with open("people.json", "r") as f:
            data = json.load(f)

        # Create the model and view.
        self.model = JsonTableModel(data)
        self.table = QTableView()
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)
        self.resize(500, 300)


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

Run this and you'll see a table view displaying the data from your JSON file, complete with column headers.

Working with Nested JSON

Real-world JSON is often more complex than a flat list of objects. You might have nested structures like this:

json
[
    {
        "name": "Alice",
        "age": 34,
        "address": {
            "city": "Berlin",
            "country": "Germany"
        },
        "skills": ["Python", "C++"]
    },
    {
        "name": "Bob",
        "age": 28,
        "address": {
            "city": "London",
            "country": "UK"
        },
        "skills": ["JavaScript", "CSS"]
    }
]

Save this as people_nested.json.

A QTableView is inherently a flat, two-dimensional grid. You can't directly display nested structures in it, but you can flatten the data in your model. The model is the perfect place to do this — your original data stays complex, but the model presents a simplified view to Qt.

Here's a model that handles nested dictionaries by using dot-notation column definitions:

python
class NestedJsonTableModel(QAbstractTableModel):
    def __init__(self, data, columns):
        super().__init__()
        self._data = data
        # columns is a list of tuples: (header_label, key_path)
        # key_path is a dot-separated string like "address.city"
        self._columns = columns

    def rowCount(self, parent=None):
        return len(self._data)

    def columnCount(self, parent=None):
        return len(self._columns)

    def _resolve(self, obj, key_path):
        """Walk into a nested dict using a dot-separated path."""
        keys = key_path.split(".")
        for key in keys:
            if isinstance(obj, dict):
                obj = obj.get(key, "")
            else:
                return ""
        return obj

    def data(self, index, role=Qt.ItemDataRole.DisplayRole):
        if role == Qt.ItemDataRole.DisplayRole:
            row = self._data[index.row()]
            _, key_path = self._columns[index.column()]
            value = self._resolve(row, key_path)
            # Handle lists by joining them into a string.
            if isinstance(value, list):
                return ", ".join(str(v) for v in value)
            return str(value)
        return None

    def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
        if role == Qt.ItemDataRole.DisplayRole:
            if orientation == Qt.Orientation.Horizontal:
                label, _ = self._columns[section]
                return label
        return None

The _resolve method walks through the nested dictionary using a dot-separated path. So "address.city" first looks up "address" (getting a nested dict), then looks up "city" inside that. This keeps the logic clean and reusable.

You define the columns you want to display when creating the model:

python
columns = [
    ("Name", "name"),
    ("Age", "age"),
    ("City", "address.city"),
    ("Country", "address.country"),
    ("Skills", "skills"),
]

with open("people_nested.json", "r") as f:
    data = json.load(f)

model = NestedJsonTableModel(data, columns)

This approach gives you full control over which parts of your data appear in the table and in what order. Your JSON stays as-is — the model handles the mapping.

Complete Working Example with Nested Data

Here's the full application with the nested JSON model:

python
import sys
import json

from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView
from PyQt6.QtCore import Qt, QAbstractTableModel


class NestedJsonTableModel(QAbstractTableModel):
    """A table model that can display data from nested JSON structures."""

    def __init__(self, data, columns):
        super().__init__()
        self._data = data
        # columns: list of (header_label, dot_separated_key_path)
        self._columns = columns

    def rowCount(self, parent=None):
        return len(self._data)

    def columnCount(self, parent=None):
        return len(self._columns)

    def _resolve(self, obj, key_path):
        """Walk into a nested dict using a dot-separated path."""
        keys = key_path.split(".")
        for key in keys:
            if isinstance(obj, dict):
                obj = obj.get(key, "")
            else:
                return ""
        return obj

    def data(self, index, role=Qt.ItemDataRole.DisplayRole):
        if role == Qt.ItemDataRole.DisplayRole:
            row = self._data[index.row()]
            _, key_path = self._columns[index.column()]
            value = self._resolve(row, key_path)
            if isinstance(value, list):
                return ", ".join(str(v) for v in value)
            return str(value)
        return None

    def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
        if role == Qt.ItemDataRole.DisplayRole:
            if orientation == Qt.Orientation.Horizontal:
                label, _ = self._columns[section]
                return label
        return None


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Nested JSON Table Model")

        # Define which fields to show and how to reach them.
        columns = [
            ("Name", "name"),
            ("Age", "age"),
            ("City", "address.city"),
            ("Country", "address.country"),
            ("Skills", "skills"),
        ]

        # Load the nested JSON data.
        with open("people_nested.json", "r") as f:
            data = json.load(f)

        self.model = NestedJsonTableModel(data, columns)
        self.table = QTableView()
        self.table.setModel(self.model)

        # Resize columns to fit their contents.
        self.table.resizeColumnsToContents()

        self.setCentralWidget(self.table)
        self.resize(600, 300)


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

Adding Sorting with QSortFilterProxyModel

Once you have your model working, adding sorting is straightforward using QSortFilterProxyModel. This sits between your model and the view, providing sort and filter capabilities without modifying your original data. For more details on sorting and filtering tables, see our guide to sorting and filtering in Qt Model/View.

python
import sys
import json

from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel


class NestedJsonTableModel(QAbstractTableModel):
    def __init__(self, data, columns):
        super().__init__()
        self._data = data
        self._columns = columns

    def rowCount(self, parent=None):
        return len(self._data)

    def columnCount(self, parent=None):
        return len(self._columns)

    def _resolve(self, obj, key_path):
        keys = key_path.split(".")
        for key in keys:
            if isinstance(obj, dict):
                obj = obj.get(key, "")
            else:
                return ""
        return obj

    def data(self, index, role=Qt.ItemDataRole.DisplayRole):
        if role == Qt.ItemDataRole.DisplayRole:
            row = self._data[index.row()]
            _, key_path = self._columns[index.column()]
            value = self._resolve(row, key_path)
            if isinstance(value, list):
                return ", ".join(str(v) for v in value)
            return str(value)
        return None

    def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
        if role == Qt.ItemDataRole.DisplayRole:
            if orientation == Qt.Orientation.Horizontal:
                label, _ = self._columns[section]
                return label
        return None


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Sortable JSON Table")

        columns = [
            ("Name", "name"),
            ("Age", "age"),
            ("City", "address.city"),
            ("Country", "address.country"),
            ("Skills", "skills"),
        ]

        with open("people_nested.json", "r") as f:
            data = json.load(f)

        # Create the source model.
        self.source_model = NestedJsonTableModel(data, columns)

        # Wrap it in a proxy model for sorting.
        self.proxy_model = QSortFilterProxyModel()
        self.proxy_model.setSourceModel(self.source_model)

        self.table = QTableView()
        self.table.setModel(self.proxy_model)
        self.table.setSortingEnabled(True)
        self.table.resizeColumnsToContents()

        self.setCentralWidget(self.table)
        self.resize(600, 300)


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

Click any column header to sort by that column. Click again to reverse the sort order. The QSortFilterProxyModel handles all of this automatically — you don't need to write any sorting logic yourself.

Summary

The Model/View architecture in PyQt6 is designed to work with whatever data you have. The model is a translation layer: it takes your data — whether it's a flat list, nested JSON, a database query, or anything else — and presents it in the row-and-column format that Qt's views expect.

When working with complex data structures, keep these principles in mind:

  • Your data stays as-is. You don't need to restructure your JSON or flatten your database. The model handles the translation.
  • Define your columns explicitly. For nested data, decide upfront which fields you want to display and how to reach them.
  • Use helper methods. A small utility like _resolve() keeps your data() method clean and makes it easy to access deeply nested values.
  • Layer on functionality. Use QSortFilterProxyModel to add sorting and filtering without touching your model code.

If you're working with tabular data from pandas or numpy instead of JSON, take a look at our tutorial on displaying pandas DataFrames in QTableView. For making your table cells editable, see our guide on editing data in a PyQt6 QTableView.

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

Using Complex Data Sources with PyQt6 Model/View Architecture 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.