Why QTableView Row Deletion Only Works Once

Understanding how beginRemoveRows, endRemoveRows, and your underlying data work together in Qt model/view
Heads up! You've already completed this tutorial.

I've implemented removeRows on my QAbstractTableModel and it successfully deletes all the rows from my QTableView, 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 does rowCount keep 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:

  1. Call beginRemoveRows — this tells the view "I'm about to remove some rows."
  2. Actually modify your underlying data — delete the rows from whatever data structure you're using (a list, a list of lists, etc.).
  3. 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:

python
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:

python
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.

Over 15,000 developers have bought Create GUI Applications with Python & Qt!
Create GUI Applications with Python & Qt6
Get the book

Downloadable ebook (PDF, ePub) & Complete Source code

[[ discount.discount_pc ]]% OFF for the next [[ discount.duration ]] [[discount.description ]] with the code [[ discount.coupon_code ]]

Purchasing Power Parity

Developers in [[ country ]] get [[ discount.discount_pc ]]% OFF on all books & courses with code [[ discount.coupon_code ]]

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:

python
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.

python
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:

  • beginRemoveRows and endRemoveRows are 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 rowCount reflects 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.

PyQt/PySide Development Services — 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.

Find out More

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak
Martin Fitzpatrick

Why QTableView Row Deletion Only Works Once was written by Martin Fitzpatrick.

Martin Fitzpatrick is the creator of Python GUIs, and has been developing Python/Qt applications for the past 12+ years. He has written a number of popular Python books and provides Python software development & consulting for teams and startups.