If you've set up a QTableView with a custom QComboBox delegate, you might run into a frustrating problem: when the underlying data in your model changes, the ComboBox items don't update. The delegate keeps showing the old values, even though your model clearly has the new data.
In this article, we'll walk through why this happens and how to fix it properly using model reset signals.
The Problem: Stale ComboBox Data
Imagine you have a QTableView backed by a QAbstractTableModel, with a QItemDelegate subclass that creates a QComboBox editor for a specific column. Something like this:
class ComboDelegate(QItemDelegate):
"""
A delegate that places a QComboBox in every cell of the given column.
"""
def __init__(self, parent):
super().__init__(parent)
def createEditor(self, parent, option, index):
combobox = QComboBox(parent)
version_list = []
for item in index.data():
if item not in version_list:
version_list.append(item)
combobox.addItems(version_list)
return combobox
def setEditorData(self, editor, index):
value = index.data()
if value:
max_val = len(value)
editor.setCurrentIndex(max_val - 1)
def setModelData(self, editor, model, index):
pass
You assign this delegate to a column on your table view:
delegate = ComboDelegate(self)
self.ui.table_view.setItemDelegateForColumn(col_id, delegate)
Everything looks great on the first load. But then your data changes — maybe you fetch fresh results from a database or an API — and the ComboBoxes still show the old items.
You might try recreating the delegate, or removing and re-adding it. None of that helps, because the issue isn't with the delegate itself — it's with the view not knowing that it needs to re-query the model.
Why This Happens
When a QTableView displays data, it caches information about what it's showing. The delegate's createEditor method is called when a cell enters editing mode, and it reads the data from the model at that point. But the view won't automatically ask the delegate to recreate editors or refresh displayed data unless it knows the model has changed.
The view listens for specific signals from the model to know when data has been added, removed, or changed. If your model's data changes but the model doesn't emit the right signals, the view has no reason to refresh anything.
Resetting the Model
The simplest way to tell the view that the entire dataset has changed is to perform a model reset. You do this by calling beginResetModel() and endResetModel() on your model, wrapping the point where you update the data:
self.table_model.beginResetModel()
# Update your internal data here
self.table_model._data = new_data # or however you store your data
self.table_model.endResetModel()
The beginResetModel() call tells any connected views that the model is about to change completely. The endResetModel() call tells them the change is done and they should refresh everything — layout, data, delegates, all of it.
This pair of calls causes the view to discard any cached state and re-request everything from the model. When the delegate's createEditor is next called, it will pull the fresh data from the model via index.data(), and your ComboBoxes will show the updated items.
A Complete Working Example
Here's a full example that demonstrates this in action. The table has a column where each cell contains a list of version strings, displayed via a ComboBox delegate. A button lets you swap in a completely new dataset — and because we use beginResetModel()/endResetModel(), the ComboBoxes update correctly.
import sys
from PyQt6.QtCore import QAbstractTableModel, Qt
from PyQt6.QtWidgets import (
QApplication,
QComboBox,
QHBoxLayout,
QItemDelegate,
QPushButton,
QTableView,
QVBoxLayout,
QWidget,
)
INITIAL_DATA = [
["Alice", ["1.0", "1.1", "1.2"]],
["Bob", ["2.0", "2.1"]],
["Charlie", ["3.0", "3.1", "3.2", "3.3"]],
]
UPDATED_DATA = [
["Alice", ["4.0", "4.1"]],
["Bob", ["5.0", "5.1", "5.2", "5.3"]],
["Charlie", ["6.0"]],
]
class TableModel(QAbstractTableModel):
def __init__(self, data):
super().__init__()
self._data = data
self._headers = ["Name", "Versions"]
def rowCount(self, parent=None):
return len(self._data)
def columnCount(self, parent=None):
return len(self._headers)
def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return None
if role == Qt.DisplayRole:
return self._data[index.row()][index.column()]
return None
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return self._headers[section]
return None
def update_data(self, new_data):
self.beginResetModel()
self._data = new_data
self.endResetModel()
class ComboDelegate(QItemDelegate):
"""
A delegate that places a QComboBox in every cell of the given column.
"""
def __init__(self, parent=None):
super().__init__(parent)
def createEditor(self, parent, option, index):
combobox = QComboBox(parent)
items = index.data()
if items and isinstance(items, list):
# Remove duplicates while preserving order.
seen = set()
unique_items = []
for item in items:
if item not in seen:
seen.add(item)
unique_items.append(item)
combobox.addItems(unique_items)
return combobox
def setEditorData(self, editor, index):
value = index.data()
if value and isinstance(value, list):
# Default to the last item in the list.
editor.setCurrentIndex(len(value) - 1)
def setModelData(self, editor, model, index):
# Store the selected text back into the model if needed.
pass
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("ComboBox Delegate Refresh Example")
self.resize(400, 250)
layout = QVBoxLayout()
self.setLayout(layout)
self.table_view = QTableView()
layout.addWidget(self.table_view)
# Set up the model with initial data.
self.model = TableModel(INITIAL_DATA)
self.table_view.setModel(self.model)
# Assign the ComboBox delegate to column 1 (the "Versions" column).
delegate = ComboDelegate(self.table_view)
self.table_view.setItemDelegateForColumn(1, delegate)
# Open persistent editors so the ComboBoxes are always visible.
for row in range(self.model.rowCount()):
index = self.model.index(row, 1)
self.table_view.openPersistentEditor(index)
# Buttons to swap data.
button_layout = QHBoxLayout()
layout.addLayout(button_layout)
btn_initial = QPushButton("Load Initial Data")
btn_initial.clicked.connect(self.load_initial_data)
button_layout.addWidget(btn_initial)
btn_updated = QPushButton("Load Updated Data")
btn_updated.clicked.connect(self.load_updated_data)
button_layout.addWidget(btn_updated)
def load_initial_data(self):
self.model.update_data(INITIAL_DATA)
self._reopen_editors()
def load_updated_data(self):
self.model.update_data(UPDATED_DATA)
self._reopen_editors()
def _reopen_editors(self):
"""Re-open persistent editors after a model reset."""
for row in range(self.model.rowCount()):
index = self.model.index(row, 1)
self.table_view.openPersistentEditor(index)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Run this and you'll see a table with three rows. Each row has a name and a ComboBox filled with version strings. Click Load Updated Data and the ComboBoxes refresh with completely new values. Click Load Initial Data to swap back.

What's Happening Behind the Scenes
When you call beginResetModel(), the view receives a signal that the model is about to undergo a major change. It discards all its internal state — cached data, persistent editors, selections, everything. When endResetModel() fires, the view rebuilds itself from scratch by querying the model again.
This is why we also need to call openPersistentEditor() again after each reset. Persistent editors are part of the view's state that gets discarded during the reset. If you're using regular (non-persistent) editors — the kind that only appear when a user double-clicks a cell — you don't need to worry about this step, since the editor will be created fresh the next time the user interacts with the cell.
When to Use Model Reset vs. Other Signals
A full model reset is the right tool when a large portion (or all) of your data has changed. If only a single cell's data has changed, emitting dataChanged for that specific index is more efficient and doesn't disrupt the user's current selection or scroll position:
# For a single cell change:
index = self.index(row, column)
self.dataChanged.emit(index, index, [Qt.DisplayRole])
For insertions and removals of rows, use beginInsertRows()/endInsertRows() and beginRemoveRows()/endRemoveRows() respectively. These targeted signals let the view update only what's necessary, which keeps things smooth — especially in larger tables.
The full model reset is the "nuclear option," but when your entire dataset is being swapped out (as in this scenario), it's exactly what you want.
Summary
When your QComboBox delegates in a QTableView aren't updating after a data change, the fix is straightforward: wrap your data update in beginResetModel() and endResetModel(). This tells the view to throw away its cached state and rebuild everything from the model, ensuring your delegates pick up the fresh data the next time they create an editor.
Bring Your PyQt/PySide Application to Market
Stuck in development hell? I'll help you get your project focused, finished and released. Benefit from years of practical experience releasing software with Python.