Retrieve underlying data object from QAbstractTableModel

How to get the original Python object back from a selected row in a QTableView
Heads up! You've already completed this tutorial.

I have a custom QAbstractTableModel backed by a list of Python objects (e.g. Student). When the user selects one or more rows in a QTableView and clicks a button, how do I get back the original Student object for each selected row — especially when the table has been sorted?

This is a very common situation when working with Qt's Model/View architecture. You have a list of Python objects powering your table, the user interacts with the view, and you need to map what the user selected back to the underlying data. It works fine when the table isn't sorted, but the moment you enable sorting through a QSortFilterProxyModel, the row numbers in the view no longer match the row numbers in your original list.

Let's walk through how to solve this properly.

The problem: view rows vs. model rows

When you display data directly from your model (no sorting, no filtering), the row index you get from the selection matches the index of the item in your list. So self.students[row] gives you the right object.

But as soon as you put a QSortFilterProxyModel between your model and the view (which is the standard way to enable sorting and filtering in table views), the view's row numbers refer to the proxy model, not your source model. Row 0 in the view might actually be row 47 in your original list.

The solution: use the model to retrieve your objects

There are two parts to this:

  1. Store a reference to the original object in the model itself, so you can ask the model for it using a custom role or a helper method.
  2. Map proxy indexes back to source indexes when using a sort/filter proxy.

Let's build this up step by step.

Setting up the Student class

Here's a simple Student class to work with:

python
class Student:
    def __init__(self, section, last_name, first_name, email, sn_folder):
        self.section = section
        self.last_name = last_name
        self.first_name = first_name
        self.email = email
        self.sn_folder = sn_folder

    def __str__(self):
        return f"{self.last_name}, {self.first_name}"

    def __repr__(self):
        return self.__str__()

Building the table model with a custom data role

Qt's data() method works with roles. The built-in Qt.ItemDataRole.DisplayRole is what the view uses to get the text to show. You can define your own custom role to return the entire Python object for a given row.

To create a custom role, pick an integer value starting from Qt.ItemDataRole.UserRole (which is guaranteed not to conflict with built-in roles):

python
from PyQt6 import QtCore
from PyQt6.QtCore import Qt


class StudentTableModel(QtCore.QAbstractTableModel):

    COLUMNS = ["Last Name", "First Name", "Section"]

    def __init__(self, students=None, parent=None):
        super().__init__(parent)
        self.students = students if students is not None else []

    def rowCount(self, index=QtCore.QModelIndex()):
        return len(self.students)

    def columnCount(self, index=QtCore.QModelIndex()):
        return len(self.COLUMNS)

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

    def data(self, index, role=Qt.ItemDataRole.DisplayRole):
        if not index.isValid():
            return None

        if not 0 <= index.row() < len(self.students):
            return None

        student = self.students[index.row()]

        if role == Qt.ItemDataRole.DisplayRole:
            if index.column() == 0:
                return student.last_name
            elif index.column() == 1:
                return student.first_name
            elif index.column() == 2:
                return student.section

        if role == Qt.ItemDataRole.UserRole:
            return student

        return None

The line to pay attention to is:

python
if role == Qt.ItemDataRole.UserRole:
    return student

This means that for any column in a given row, if you request UserRole, you get back the original Student object. The column doesn't matter here — every column in the same row comes from the same student.

Retrieving objects from the selection

Now, when the user selects rows and clicks a button, you can get the Student objects like this:

python
def go_clicked(self):
    for index in self.table_view.selectionModel().selectedRows():
        student = index.data(Qt.ItemDataRole.UserRole)
        print(student, student.email, student.sn_folder)

That's it. You call .data() on the QModelIndex with your custom role, and Qt routes that call through to your model's data() method. You get your Python object back directly — no need to manually look up list indices.

Handling sorting with QSortFilterProxyModel

If you're using a QSortFilterProxyModel to allow the user to sort the table by clicking column headers, the indexes returned by selectionModel().selectedRows() are proxy indexes. You need to map them back to the source model to get the correct data.

Here's how to set up the proxy:

python
from PyQt6.QtCore import QSortFilterProxyModel

# In your main window setup:
self.model = StudentTableModel(students)
self.proxy_model = QSortFilterProxyModel()
self.proxy_model.setSourceModel(self.model)

self.table_view.setModel(self.proxy_model)
self.table_view.setSortingEnabled(True)

When retrieving selected objects, you now map the proxy indexes back to the source model:

python
def go_clicked(self):
    for proxy_index in self.table_view.selectionModel().selectedRows():
        source_index = self.proxy_model.mapToSource(proxy_index)
        student = self.model.data(source_index, Qt.ItemDataRole.UserRole)
        print(student, student.email, student.sn_folder)

mapToSource() translates the proxy's row number back to the original model's row number, so you always get the correct Student regardless of how the table is sorted or filtered.

Actually, there's an even simpler approach here. Since .data() on a proxy index automatically forwards to the source model (applying mapToSource internally for you), this also works:

python
def go_clicked(self):
    for index in self.table_view.selectionModel().selectedRows():
        student = index.data(Qt.ItemDataRole.UserRole)
        print(student, student.email, student.sn_folder)

The proxy model handles the mapping automatically when you call .data() on the index. The explicit mapToSource approach is useful when you need the source index itself (for example, to modify or remove items from the underlying list), but for simply reading data, calling .data() on the proxy index works perfectly.

Complete working example

Here's a full application you can copy and run. It creates a table of students, enables sorting, and prints the selected Student objects when you click the button:

python
import sys
from PyQt6 import QtCore, QtWidgets
from PyQt6.QtCore import Qt, QSortFilterProxyModel


class Student:
    def __init__(self, section, last_name, first_name, email, sn_folder):
        self.section = section
        self.last_name = last_name
        self.first_name = first_name
        self.email = email
        self.sn_folder = sn_folder

    def __str__(self):
        return f"{self.last_name}, {self.first_name}"

    def __repr__(self):
        return self.__str__()


class StudentTableModel(QtCore.QAbstractTableModel):

    COLUMNS = ["Last Name", "First Name", "Section"]

    def __init__(self, students=None, parent=None):
        super().__init__(parent)
        self.students = students if students is not None else []

    def rowCount(self, index=QtCore.QModelIndex()):
        return len(self.students)

    def columnCount(self, index=QtCore.QModelIndex()):
        return len(self.COLUMNS)

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

    def data(self, index, role=Qt.ItemDataRole.DisplayRole):
        if not index.isValid():
            return None

        if not 0 <= index.row() < len(self.students):
            return None

        student = self.students[index.row()]

        if role == Qt.ItemDataRole.DisplayRole:
            if index.column() == 0:
                return student.last_name
            elif index.column() == 1:
                return student.first_name
            elif index.column() == 2:
                return student.section

        if role == Qt.ItemDataRole.UserRole:
            return student

        return None


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Student Table Example")
        self.resize(600, 400)

        # Create some sample students.
        students = [
            Student("A", "Smith", "Alice", "alice@example.com", "/path/alice"),
            Student("B", "Johnson", "Bob", "bob@example.com", "/path/bob"),
            Student("A", "Williams", "Charlie", "charlie@example.com", "/path/charlie"),
            Student("C", "Brown", "Diana", "diana@example.com", "/path/diana"),
            Student("B", "Davis", "Edward", "edward@example.com", "/path/edward"),
            Student("A", "Miller", "Fiona", "fiona@example.com", "/path/fiona"),
        ]

        # Set up the source model and sort/filter proxy.
        self.source_model = StudentTableModel(students)
        self.proxy_model = QSortFilterProxyModel()
        self.proxy_model.setSourceModel(self.source_model)

        # Set up the table view.
        self.table_view = QtWidgets.QTableView()
        self.table_view.setModel(self.proxy_model)
        self.table_view.setSortingEnabled(True)
        self.table_view.setSelectionBehavior(
            QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows
        )

        # Create the button.
        self.button = QtWidgets.QPushButton("Get Selected Students")
        self.button.clicked.connect(self.go_clicked)

        # Layout.
        layout = QtWidgets.QVBoxLayout()
        layout.addWidget(self.table_view)
        layout.addWidget(self.button)

        container = QtWidgets.QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

    def go_clicked(self):
        selected_indexes = self.table_view.selectionModel().selectedRows()

        if not selected_indexes:
            print("No rows selected.")
            return

        for index in selected_indexes:
            # .data() on a proxy index automatically forwards to the
            # source model, so this works even when the table is sorted.
            student = index.data(Qt.ItemDataRole.UserRole)
            print(
                f"Selected: {student} | "
                f"Email: {student.email} | "
                f"Folder: {student.sn_folder}"
            )


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

Try running this, then click a column header to sort the table. Select one or more rows (hold Ctrl or Shift to multi-select) and click the button. You'll see the correct student information printed every time, regardless of the current sort order.

Summary

When working with QAbstractTableModel backed by Python objects:

  • Use Qt.ItemDataRole.UserRole in your model's data() method to return the original Python object for any given row.
  • Call .data(Qt.ItemDataRole.UserRole) on the selected QModelIndex to retrieve that object.
  • This works through proxy models too — calling .data() on a proxy index automatically maps back to the source model for you.
  • If you need the actual source index (not just the data), use proxy_model.mapToSource(proxy_index) to convert it explicitly.

This pattern keeps your code clean and avoids fragile index-based lookups into your original list, which is especially important once sorting or filtering is involved. For displaying tabular data from numpy arrays or pandas DataFrames in a QTableView, see our QTableView with numpy and pandas tutorial. If you're looking to understand signals and slots for connecting your button clicks and other events, that's also essential knowledge for this kind of interactive application.

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

Retrieve underlying data object from 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.