I'm using a QTableView to display data, and would like to limit the choices in some of the fields using a drop-down. I can use
QComboBoxto provide a list of choices in a normal UI, but how can I do that in a table view?
When you're working with QTableView in PyQt6, you'll sometimes want cells that offer a dropdown selection instead of plain text. A QComboBox is the natural fit here — but embedding one inside a table view takes a bit of wiring up.
In this tutorial, we'll walk through how to use a QItemDelegate to place a QComboBox into specific cells of a QTableView. We'll also cover how to populate each combo box with different items per row, and how to retrieve the selected value so you can use it elsewhere in your application.
How delegates work in Qt's Model/View framework
Qt's Model/View architecture separates your data (the model) from how it's displayed (the view). Between these two sits the delegate, which controls how individual cells are rendered and edited. When you want a cell to use a widget like a combo box instead of a plain text editor, you create a custom delegate.
The delegate has a few methods you'll override:
createEditor()— creates the widget (in our case, aQComboBox) when the user starts editing a cell.setEditorData()— populates the editor widget with the current data from the model.setModelData()— writes the user's selection back into the model.updateEditorGeometry()— makes sure the widget is sized and positioned correctly inside the cell.
Let's build this up step by step.
Setting up the model and view
First, let's create a simple application with a QTableView and a QStandardItemModel. Each row will represent a software package, and one of the columns will hold a list of available versions. We'll store those version lists directly in the model data, so each row can have its own set of options.
import sys
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QTableView, QComboBox, QItemDelegate,
)
from PyQt6.QtGui import QStandardItemModel, QStandardItem
from PyQt6.QtCore import Qt, QItemDataRole
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("QComboBox in QTableView")
self.table = QTableView()
self.setCentralWidget(self.table)
# Create a model with 3 rows and 2 columns.
self.model = QStandardItemModel(3, 2)
self.model.setHorizontalHeaderLabels(["Package", "Version"])
# Each row has a package name and a list of available versions.
packages = [
("Widget Library", ["1.0", "1.1", "2.0", "2.1"]),
("Data Toolkit", ["0.9", "1.0"]),
("Render Engine", ["3.0", "3.1", "3.2", "4.0"]),
]
for row, (name, versions) in enumerate(packages):
# Column 0: package name (plain text).
self.model.setItem(row, 0, QStandardItem(name))
# Column 1: store the version list in the item's data.
# We use Qt.ItemDataRole.UserRole to keep the full list alongside the display text.
item = QStandardItem(versions[-1]) # Display the latest version by default.
item.setData(versions, Qt.ItemDataRole.UserRole)
self.model.setItem(row, 1, item)
self.table.setModel(self.model)
# Apply our custom delegate to column 1.
delegate = ComboDelegate(self.table)
self.table.setItemDelegateForColumn(1, delegate)
self.resize(400, 200)
Notice how we store the list of versions using Qt.ItemDataRole.UserRole. This is a custom data role — it lets us attach extra information to a model item without interfering with the text that's displayed (which uses Qt.ItemDataRole.DisplayRole). Each row gets its own version list, so when the combo box opens, it will show only the versions relevant to that row.
Creating the combo box delegate
Now let's write the ComboDelegate class. This is where the combo box gets created and connected to the model.
class ComboDelegate(QItemDelegate):
"""
A delegate that places a QComboBox in cells of the assigned column.
"""
def createEditor(self, parent, option, index):
# Create the combo box and populate it with the version list for this row.
combo = QComboBox(parent)
versions = index.data(Qt.ItemDataRole.UserRole)
if versions:
combo.addItems(versions)
return combo
def setEditorData(self, editor, index):
# Set the combo box to show the currently selected value.
current_text = index.data(Qt.ItemDataRole.DisplayRole)
idx = editor.findText(current_text)
if idx >= 0:
editor.setCurrentIndex(idx)
def setModelData(self, editor, model, index):
# Write the selected value back into the model.
model.setData(index, editor.currentText(), Qt.ItemDataRole.DisplayRole)
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
Let's walk through each method:
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.
createEditor() is called when the user double-clicks (or otherwise activates) a cell in column 1. We create a fresh QComboBox, pull the version list from Qt.ItemDataRole.UserRole for that specific row, and add those items to the combo box. Because each row stores its own list, different rows will show different options.
setEditorData() makes sure the combo box starts with the right item selected. We read the current display text from the model and find the matching entry in the combo box.
setModelData() fires when the user finishes editing (for example, by clicking away from the cell). It takes whatever the user selected in the combo box and writes it back into the model's DisplayRole.
updateEditorGeometry() simply ensures the combo box fills the cell neatly.
Running the application
Add the standard entry point at the bottom of your script:
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Run the script and double-click any cell in the "Version" column. You'll see a combo box appear with the version options for that specific row. Select a value, click away, and the cell updates.

Getting the selected value
After the user makes a selection, the value is stored in the model. You can read it at any time:
# Read the selected version for row 0.
selected = self.model.item(0, 1).text()
print(f"Row 0 selected version: {selected}")
If you want to react immediately when a selection changes, you can connect to the model's dataChanged signal. If you're new to how signals work in Qt, see our guide on signals, slots and events:
self.model.dataChanged.connect(self.on_data_changed)
def on_data_changed(self, top_left, bottom_right, roles):
if top_left.column() == 1:
row = top_left.row()
value = top_left.data(Qt.ItemDataRole.DisplayRole)
print(f"Row {row} version changed to: {value}")
This approach keeps things nicely separate — you're working through the model rather than trying to hold references to individual combo box widgets. The combo boxes are created and destroyed as the user interacts with cells.
Setting a value programmatically
To change a cell's value from code, update the model directly:
# Set row 2's version to "3.1".
self.model.item(2, 1).setText("3.1")
The next time the user opens the combo box on that row, the delegate's setEditorData() will position the combo box on "3.1".
You can also update the list of available versions for a row:
# Add a new version to row 1's options.
item = self.model.item(1, 1)
versions = item.data(Qt.ItemDataRole.UserRole)
versions.append("1.1")
item.setData(versions, Qt.ItemDataRole.UserRole)
Why each row gets its own combo box items
A common stumbling block is ending up with the same items in every combo box across the column. This happens when you store the item list on the delegate itself (as a single shared list) rather than on the model. Since the delegate is shared across all rows, any list stored on it will be the same everywhere.
The solution, as we've done here, is to store per-row data in the model using Qt.ItemDataRole.UserRole. Each call to createEditor() reads from the specific index it's given, so each row naturally gets its own set of options. This is a pattern you'll use often when different rows need different editor configurations.
Complete code
Here's the full working example in one block:
import sys
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QTableView, QComboBox, QItemDelegate,
)
from PyQt6.QtGui import QStandardItemModel, QStandardItem
from PyQt6.QtCore import Qt
class ComboDelegate(QItemDelegate):
"""
A delegate that places a QComboBox in cells of the assigned column.
"""
def createEditor(self, parent, option, index):
combo = QComboBox(parent)
versions = index.data(Qt.ItemDataRole.UserRole)
if versions:
combo.addItems(versions)
return combo
def setEditorData(self, editor, index):
current_text = index.data(Qt.ItemDataRole.DisplayRole)
idx = editor.findText(current_text)
if idx >= 0:
editor.setCurrentIndex(idx)
def setModelData(self, editor, model, index):
model.setData(index, editor.currentText(), Qt.ItemDataRole.DisplayRole)
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("QComboBox in QTableView")
self.table = QTableView()
self.setCentralWidget(self.table)
self.model = QStandardItemModel(3, 2)
self.model.setHorizontalHeaderLabels(["Package", "Version"])
packages = [
("Widget Library", ["1.0", "1.1", "2.0", "2.1"]),
("Data Toolkit", ["0.9", "1.0"]),
("Render Engine", ["3.0", "3.1", "3.2", "4.0"]),
]
for row, (name, versions) in enumerate(packages):
self.model.setItem(row, 0, QStandardItem(name))
item = QStandardItem(versions[-1])
item.setData(versions, Qt.ItemDataRole.UserRole)
self.model.setItem(row, 1, item)
self.table.setModel(self.model)
delegate = ComboDelegate(self.table)
self.table.setItemDelegateForColumn(1, delegate)
# React to changes.
self.model.dataChanged.connect(self.on_data_changed)
self.resize(400, 200)
def on_data_changed(self, top_left, bottom_right, roles):
if top_left.column() == 1:
row = top_left.row()
value = top_left.data(Qt.ItemDataRole.DisplayRole)
print(f"Row {row} version changed to: {value}")
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Wrapping up
Using a custom QItemDelegate gives you full control over how cells in a QTableView are edited. By storing per-row data in the model with Qt.ItemDataRole.UserRole, you can give each combo box its own set of items — solving the common problem of all combo boxes showing the same options.
The pattern here — store data in the model, read it in the delegate, write changes back to the model — works well beyond combo boxes. You can use the same approach to embed spin boxes, date pickers, or any other widget into your table cells. Once you're comfortable with this flow, you'll find Qt's Model/View framework surprisingly flexible. For a deeper dive into using QTableView with real-world data sources like NumPy and Pandas, see our QTableView with numpy and pandas tutorial. You can also explore how to make table cells editable for other common editing patterns.
PyQt6 Crash Course by Martin Fitzpatrick — The important parts of PyQt6 in bite-size chunks