Combo Box delegate disappears with QSortFilterProxyModel applied

Why your QTableView delegate vanishes when you add a proxy model, and how to fix it
Heads up! You've already completed this tutorial.

I have a QTableView with a QAbstractTableModel and a ComboBox delegate set on one of the columns using setItemDelegateForColumn. Everything works fine until I introduce a QSortFilterProxyModel. Once the proxy model is applied, the delegate disappears from the column. How do I fix this?

If you've set up a custom delegate on a QTableView and it works perfectly — until you add a QSortFilterProxyModel — the delegate seemingly vanishing can be confusing. The proxy model shouldn't affect your delegate at all, so what's going on?

The answer comes down to which widget you pass as the parent when creating your delegate.

The problem

Here's a typical setup that runs into this issue:

python
self.table_model = DictionaryTableModel()
self.delegate = ComboDelegate(self)  # 'self' is the QDialog or QMainWindow
self.ui.table_view.setItemDelegateForColumn(2, self.delegate)

self.proxy_model = QSortFilterProxyModel(self)
self.proxy_model.setSourceModel(self.table_model)

self.ui.table_view.setModel(self.proxy_model)

At first glance this looks reasonable. You create the delegate, assign it to a column, set up the proxy model, and point the view at it. But the delegate disappears.

The cause is subtle: when you pass self (your dialog or main window) as the parent of the delegate, the delegate's parent is not the table view. When the view later sets the proxy model, it may clean up or fail to find delegates that aren't parented to it. The delegate object can be garbage collected or simply not be associated with the view correctly.

The fix

Pass the table view itself as the parent of your delegate:

python
self.delegate = ComboDelegate(self.ui.table_view)

That's it. By making the table view the parent of the delegate, Qt correctly keeps track of the delegate throughout the lifetime of the view — even when you swap in a proxy model.

A complete working example

Let's put together a full example that demonstrates a QTableView with a combo box delegate, a source model, and a QSortFilterProxyModel — all working together. If you're new to working with table models and views in PyQt6, you may want to review the QTableView with Model/View architecture tutorial first.

python
import sys

from PyQt6.QtCore import (
    QAbstractTableModel,
    QSortFilterProxyModel,
    Qt,
)
from PyQt6.QtWidgets import (
    QApplication,
    QComboBox,
    QMainWindow,
    QStyledItemDelegate,
    QTableView,
    QVBoxLayout,
    QWidget,
    QLineEdit,
)


COLORS = ["Red", "Green", "Blue", "Yellow", "Orange"]

DATA = [
    ["Alice", "Engineer", "Red"],
    ["Bob", "Designer", "Blue"],
    ["Charlie", "Manager", "Green"],
    ["Diana", "Analyst", "Yellow"],
]


class SimpleTableModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data
        self._headers = ["Name", "Role", "Favorite Color"]

    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 in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole):
            return self._data[index.row()][index.column()]
        return None

    def setData(self, index, value, role=Qt.ItemDataRole.EditRole):
        if role == Qt.ItemDataRole.EditRole:
            self._data[index.row()][index.column()] = value
            self.dataChanged.emit(index, index, [role])
            return True
        return False

    def flags(self, index):
        return (
            Qt.ItemFlag.ItemIsSelectable
            | Qt.ItemFlag.ItemIsEnabled
            | Qt.ItemFlag.ItemIsEditable
        )

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


class ComboDelegate(QStyledItemDelegate):
    def __init__(self, parent=None, items=None):
        super().__init__(parent)
        self.items = items or []

    def createEditor(self, parent, option, index):
        combo = QComboBox(parent)
        combo.addItems(self.items)
        return combo

    def setEditorData(self, editor, index):
        value = index.data(Qt.ItemDataRole.EditRole)
        idx = editor.findText(value)
        if idx >= 0:
            editor.setCurrentIndex(idx)

    def setModelData(self, editor, model, index):
        model.setData(index, editor.currentText(), Qt.ItemDataRole.EditRole)

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Delegate + Proxy Model Example")
        self.resize(500, 300)

        # Set up the source model.
        self.table_model = SimpleTableModel(DATA)

        # Set up the proxy model for filtering/sorting.
        self.proxy_model = QSortFilterProxyModel(self)
        self.proxy_model.setSourceModel(self.table_model)
        self.proxy_model.setFilterKeyColumn(0)  # Filter by the Name column.
        self.proxy_model.setFilterCaseSensitivity(
            Qt.CaseSensitivity.CaseInsensitive
        )

        # Set up the table view.
        self.table_view = QTableView()
        self.table_view.setModel(self.proxy_model)
        self.table_view.setSortingEnabled(True)

        # Create the delegate with the TABLE VIEW as parent.
        self.delegate = ComboDelegate(self.table_view, items=COLORS)
        self.table_view.setItemDelegateForColumn(2, self.delegate)

        # Add a filter input field.
        self.filter_input = QLineEdit()
        self.filter_input.setPlaceholderText("Type to filter by name...")
        self.filter_input.textChanged.connect(
            self.proxy_model.setFilterFixedString
        )

        # Layout.
        layout = QVBoxLayout()
        layout.addWidget(self.filter_input)
        layout.addWidget(self.table_view)

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


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

Run this and you'll see a table with four rows. The "Favorite Color" column uses a combo box delegate — double-click any cell in that column and a dropdown appears with color options. You can type in the filter box at the top to filter rows by name, and click column headers to sort. The delegate stays in place throughout.

Why the parent matters

In Qt, the parent of a widget determines its lifetime and ownership. When you call setItemDelegateForColumn, the view expects to manage that delegate. If the delegate is parented to something else (like the main window), the view doesn't fully "own" it. In certain situations — particularly when models are swapped or proxy models are introduced — this mismatch can cause the delegate to be lost or garbage collected by Python.

By parenting the delegate to the table view, you ensure that:

  • The delegate lives as long as the view does.
  • The view can properly manage the delegate when models change.
  • Python's garbage collector won't clean up the delegate unexpectedly.

This is a good habit to follow any time you create a delegate: always pass the table view as the parent.

Summary

When using a QSortFilterProxyModel with a QTableView that has custom delegates, make sure you create your delegate with the table view as its parent:

python
# Do this:
self.delegate = ComboDelegate(self.table_view)

# Instead of this:
self.delegate = ComboDelegate(self)

The proxy model itself doesn't interfere with delegates at all — delegates are a view-level concept, completely independent of the model. The real issue is object ownership in Qt's parent-child hierarchy. Getting the parent right keeps everything working smoothly.

For more on sorting and filtering tables, see our guide on sorting and filtering in QTableView. If you want to understand the broader Model/View architecture that underpins QTableView and proxy models, take a look at the PyQt6 Model/View Architecture tutorial. You can also learn more about editing data in a PyQt6 QTableView with custom delegates.

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

Combo Box delegate disappears with QSortFilterProxyModel applied 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.