How do you implement
insertRows()andremoveRows()on aQAbstractTableModeland wire them up in aQMainWindowso that rows are properly added and deleted from both the model and the view?
When you're building a table-based interface in PyQt6 using the model/view architecture, one of the first things you'll want to do beyond displaying data is modifying it — specifically, inserting new rows and removing existing ones. The QAbstractTableModel class provides a framework for this, but you need to implement the actual logic yourself.
In this tutorial, we'll walk through how to properly implement insertRows() and removeRows() on a custom QAbstractTableModel, connect those methods to toolbar actions, and make sure the view stays in sync. We'll also cover what to do if you're using a QSortFilterProxyModel between your model and view.
Setting up the table model
Let's start with a basic custom table model that stores its data as a list of lists. Each inner list represents a row, and each element within a row represents a column value.
from PyQt6 import QtCore
from PyQt6.QtCore import Qt
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super().__init__()
self._data = data
def data(self, index, role):
if role == Qt.ItemDataRole.DisplayRole:
value = self._data[index.row()][index.column()]
return str(value)
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._data[0])
This gives us a working read-only model. Now let's add the ability to insert and remove rows.
Implementing insertRows()
To insert rows, you override the insertRows() method. Qt's model/view framework requires you to follow a specific sequence when modifying model data:
- Call
beginInsertRows()to notify the view that rows are about to be added. - Actually modify the underlying data.
- Call
endInsertRows()to notify the view that the insertion is complete.
Here's the implementation:
def insertRows(self, position, rows, parent=QtCore.QModelIndex()):
self.beginInsertRows(parent, position, position + rows - 1)
for i in range(rows):
default_row = [""] * self.columnCount(None)
self._data.insert(position, default_row)
self.endInsertRows()
self.layoutChanged.emit()
return True
The position parameter tells us where in the list to insert, and rows tells us how many rows to insert. We create a default row filled with empty strings (one per column) and insert it into the data.
The beginInsertRows() call takes the parent index, the first row number being inserted, and the last row number being inserted. For a flat table (no tree hierarchy), the parent is always an empty QModelIndex().
After modifying the data and calling endInsertRows(), we also emit layoutChanged to make sure the view fully refreshes.
Implementing removeRows()
Removing rows follows the same pattern, using beginRemoveRows() and endRemoveRows():
def removeRows(self, position, rows, parent=QtCore.QModelIndex()):
self.beginRemoveRows(parent, position, position + rows - 1)
for i in range(rows):
del self._data[position]
self.endRemoveRows()
self.layoutChanged.emit()
return True
Notice that we always delete at the same position index inside the loop. After deleting the item at position, the next item slides down into that same index. If we incremented the position, we'd skip rows.
Connecting to the main window
With the model methods in place, we need a way for the user to trigger insertions and deletions. A simple approach is to add toolbar actions in the QMainWindow:
from PyQt6 import QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.insert_action = QtWidgets.QAction("Insert Row")
self.insert_action.triggered.connect(self.insert_row)
self.delete_action = QtWidgets.QAction("Delete Row")
self.delete_action.triggered.connect(self.delete_row)
toolbar = QtWidgets.QToolBar("Edit")
toolbar.addAction(self.insert_action)
toolbar.addAction(self.delete_action)
self.addToolBar(toolbar)
self.table = QtWidgets.QTableView()
data = [
[1, 9, 2],
[1, 0, -1],
[3, 5, 2],
[3, 3, 2],
[5, 8, 9],
]
self.model = TableModel(data)
self.table.setModel(self.model)
self.setCentralWidget(self.table)
def insert_row(self):
index = self.table.currentIndex()
self.model.insertRows(index.row(), 1)
def delete_row(self):
index = self.table.currentIndex()
self.model.removeRows(index.row(), 1)
When the user clicks "Insert Row," a new empty row is inserted at the currently selected row position. When they click "Delete Row," the currently selected row is removed.
The currentIndex() method on the QTableView returns a QModelIndex for whatever cell is currently selected. We use .row() to get the row number and pass that to our model methods.
Complete working example
Here's the full example you can copy and run:
import sys
from PyQt6 import QtCore, QtWidgets
from PyQt6.QtCore import Qt
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super().__init__()
self._data = data
def data(self, index, role):
if role == Qt.ItemDataRole.DisplayRole:
value = self._data[index.row()][index.column()]
return str(value)
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._data[0])
def insertRows(self, position, rows, parent=QtCore.QModelIndex()):
self.beginInsertRows(parent, position, position + rows - 1)
for i in range(rows):
default_row = [""] * self.columnCount(None)
self._data.insert(position, default_row)
self.endInsertRows()
self.layoutChanged.emit()
return True
def removeRows(self, position, rows, parent=QtCore.QModelIndex()):
self.beginRemoveRows(parent, position, position + rows - 1)
for i in range(rows):
del self._data[position]
self.endRemoveRows()
self.layoutChanged.emit()
return True
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.insert_action = QtWidgets.QAction("Insert Row")
self.insert_action.triggered.connect(self.insert_row)
self.delete_action = QtWidgets.QAction("Delete Row")
self.delete_action.triggered.connect(self.delete_row)
toolbar = QtWidgets.QToolBar("Edit")
toolbar.addAction(self.insert_action)
toolbar.addAction(self.delete_action)
self.addToolBar(toolbar)
self.table = QtWidgets.QTableView()
data = [
[1, 9, 2],
[1, 0, -1],
[3, 5, 2],
[3, 3, 2],
[5, 8, 9],
]
self.model = TableModel(data)
self.table.setModel(self.model)
self.setCentralWidget(self.table)
def insert_row(self):
index = self.table.currentIndex()
self.model.insertRows(index.row(), 1)
def delete_row(self):
index = self.table.currentIndex()
self.model.removeRows(index.row(), 1)
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Run this, click on a cell in the table, and then use the toolbar buttons to insert or delete rows. You'll see the table update immediately.
Working with a QSortFilterProxyModel
If you're using a QSortFilterProxyModel between your model and view (for example, to allow sorting or filtering), there's an extra step you need to take care of. The currentIndex() from the view returns an index in the proxy model's coordinate space, not the source model's. You need to map it back to the source model before passing it to insertRows() or removeRows().
Here's how the main window looks with a proxy model added:
import sys
from PyQt6 import QtCore, QtWidgets
from PyQt6.QtCore import Qt
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super().__init__()
self._data = data
def data(self, index, role):
if role == Qt.ItemDataRole.DisplayRole:
value = self._data[index.row()][index.column()]
return str(value)
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._data[0])
def insertRows(self, position, rows, parent=QtCore.QModelIndex()):
self.layoutAboutToBeChanged.emit()
self.beginInsertRows(parent, position, position + rows - 1)
for i in range(rows):
default_row = [""] * self.columnCount(None)
self._data.insert(position, default_row)
self.endInsertRows()
self.layoutChanged.emit()
return True
def removeRows(self, position, rows, parent=QtCore.QModelIndex()):
self.layoutAboutToBeChanged.emit()
self.beginRemoveRows(parent, position, position + rows - 1)
for i in range(rows):
del self._data[position]
self.endRemoveRows()
self.layoutChanged.emit()
return True
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.insert_action = QtWidgets.QAction("Insert Row")
self.insert_action.triggered.connect(self.insert_row)
self.delete_action = QtWidgets.QAction("Delete Row")
self.delete_action.triggered.connect(self.delete_row)
toolbar = QtWidgets.QToolBar("Edit")
toolbar.addAction(self.insert_action)
toolbar.addAction(self.delete_action)
self.addToolBar(toolbar)
self.table = QtWidgets.QTableView()
data = [
[1, 9, 2],
[1, 0, -1],
[3, 5, 2],
[3, 3, 2],
[5, 8, 9],
]
self.model = TableModel(data)
self.proxy_model = QtCore.QSortFilterProxyModel()
self.proxy_model.setSourceModel(self.model)
self.table.setModel(self.proxy_model)
self.table.setSortingEnabled(True)
self.table.setSelectionBehavior(
QtWidgets.QTableView.SelectionBehavior.SelectRows
)
self.setCentralWidget(self.table)
def insert_row(self):
proxy_index = self.table.currentIndex()
source_index = self.proxy_model.mapToSource(proxy_index)
self.model.insertRows(source_index.row(), 1)
def delete_row(self):
proxy_index = self.table.currentIndex()
source_index = self.proxy_model.mapToSource(proxy_index)
self.model.removeRows(source_index.row(), 1)
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
The mapToSource() method translates the proxy index back to the corresponding index in the source model. This ensures you're inserting or deleting at the correct position in the underlying data, even if the view is sorted or filtered differently.
You'll also notice that in this version, the model methods emit layoutAboutToBeChanged at the start. This signal tells the proxy model (and any other connected views) to prepare for a structural change. Without it, you might find that only the first insert or delete works, and subsequent operations cause the application to freeze or silently fail. Paired with layoutChanged at the end, this keeps everything synchronized.
PyQt/PySide 1:1 Coaching with Martin Fitzpatrick
Save yourself time and frustration. Get one on one help with your Python GUI projects. Working together with you I'll identify issues and suggest fixes, from bugs and usability to architecture and maintainability.