I've implemented
removeRowson myQAbstractTableModeland it successfully deletes all the rows from myQTableView, but only the first time. After that, clicking the delete button does nothing, even though new rows have been added. Why does deletion only work once, and why doesrowCountkeep increasing?
The methods beginRemoveRows and endRemoveRows only notify the view that data has changed — they don't actually remove anything from your underlying data. If you skip that step, the view gets out of sync with your data, and everything goes sideways.
Let's walk through why this happens and how to fix it.
How Qt's Model/View Removal Works
When you implement removeRows in a QAbstractTableModel, there are three things that need to happen, in order:
- Call
beginRemoveRows— this tells the view "I'm about to remove some rows." - Actually modify your underlying data — delete the rows from whatever data structure you're using (a list, a list of lists, etc.).
- Call
endRemoveRows— this tells the view "I'm done removing rows, update yourself."
If you skip step 2, the view thinks the rows are gone, but your data still has them. The next time the view asks your model how many rows there are (via rowCount), the model reports the original count — and now the view and the model disagree about what's going on. This mismatch is what causes the deletion to only appear to work once.
Here's the problematic version of removeRows:
def removeRows(self, position, rows=1, index=QModelIndex()):
self.beginRemoveRows(QModelIndex(), position, position + rows - 1)
# Nothing happens to self._data here!
self.endRemoveRows()
The view is notified, but the data is untouched. On the first call, the view removes the visual rows. But rowCount still returns the old length, and subsequent operations get confused.
Fixing removeRows
The fix is straightforward: actually delete the data between beginRemoveRows and endRemoveRows.
For a model where self._data is a simple list of rows, that looks like this:
def removeRows(self, position, rows=1, index=QModelIndex()):
self.beginRemoveRows(QModelIndex(), position, position + rows - 1)
for _ in range(rows):
del self._data[position]
self.endRemoveRows()
return True
We delete at position repeatedly (rather than incrementing the index) because each deletion shifts subsequent items up by one.
Handling Column-Oriented Data
In the original question, the data is stored in a column-oriented structure — self._data is a list of columns, where each column is a list of values. That means self._data[0] is all the values in column 0, self._data[1] is all the values in column 1, and so on.
With this layout, removing a "row" means removing the item at the same index from every column list:
def removeRows(self, position, rows=1, index=QModelIndex()):
self.beginRemoveRows(QModelIndex(), position, position + rows - 1)
for _ in range(rows):
for column in self._data:
del column[position]
self.endRemoveRows()
return True
This ensures that every column loses the same row entries, keeping everything aligned.
A Complete Working Example
Here's a full, runnable example that demonstrates a QTableView with an add button and a delete-all button. You can add rows and then clear the table, as many times as you like. If you're new to the model/view architecture in Qt, you may want to read our introduction to PyQt6 model/view architecture first.
import sys
from PyQt6.QtCore import QAbstractTableModel, QModelIndex, Qt
from PyQt6.QtWidgets import (
QApplication,
QHBoxLayout,
QMainWindow,
QPushButton,
QTableView,
QVBoxLayout,
QWidget,
)
class TableModel(QAbstractTableModel):
def __init__(self, data, headers):
super().__init__()
self._data = data # List of columns, each column is a list
self._headers = headers
def data(self, index, role):
if role == Qt.ItemDataRole.DisplayRole:
return self._data[index.column()][index.row()]
return None
def rowCount(self, index=QModelIndex()):
if not self._data:
return 0
return len(self._data[0])
def columnCount(self, index=QModelIndex()):
return len(self._data)
def headerData(self, section, orientation, role):
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
return self._headers[section]
return None
def add_row(self, values):
"""Add a row. values should be a list with one entry per column."""
row_index = self.rowCount()
self.beginInsertRows(QModelIndex(), row_index, row_index)
for col_index, value in enumerate(values):
self._data[col_index].append(value)
self.endInsertRows()
def removeRows(self, position, rows=1, index=QModelIndex()):
if self.rowCount() == 0:
return False
self.beginRemoveRows(QModelIndex(), position, position + rows - 1)
for _ in range(rows):
for column in self._data:
del column[position]
self.endRemoveRows()
return True
def clear_all(self):
"""Remove every row from the model."""
row_count = self.rowCount()
if row_count > 0:
self.removeRows(0, row_count)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("TableView Row Deletion Example")
# Column-oriented data: each inner list is one column.
data = [
[], # Name column
[], # Status column
]
headers = ["Name", "Status"]
self.model = TableModel(data, headers)
self.table = QTableView()
self.table.setModel(self.model)
self.counter = 0
add_button = QPushButton("Add Row")
add_button.clicked.connect(self.add_row)
delete_button = QPushButton("Delete All Rows")
delete_button.clicked.connect(self.model.clear_all)
button_layout = QHBoxLayout()
button_layout.addWidget(add_button)
button_layout.addWidget(delete_button)
layout = QVBoxLayout()
layout.addWidget(self.table)
layout.addLayout(button_layout)
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
def add_row(self):
self.counter += 1
self.model.add_row([f"Item {self.counter}", "Not Started"])
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Run this, click Add Row a few times to populate the table, then click Delete All Rows to clear it. You can repeat this as many times as you want — the deletion works every time because the underlying data is properly modified alongside the view notifications.
Summary
When implementing removeRows on a QAbstractTableModel:
beginRemoveRowsandendRemoveRowsare signals to the view, not commands that modify your data.- You must delete the actual data from your underlying data structure between these two calls.
- If your data is column-oriented (a list of column lists), remember to remove the entry at the target row index from every column.
- Always make sure
rowCountreflects the true size of your data — if it does, the view will stay in sync with your model reliably.
For more on working with QTableView and displaying data from Python data structures, see our tutorial on using QTableView with NumPy and Pandas. If you need to make your table cells editable, check out our guide on editing a PyQt6 QTableView.
Create GUI Applications with Python & Qt6 by Martin Fitzpatrick
(PyQt6 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!