If you've tried to implement drag and drop row reordering in a QTableView using a custom QAbstractTableModel, you've probably run into the prohibited icon — the red circle with a slash through it that appears when you try to drop a row. You can see the drag start, the row follows your cursor, but the table refuses to accept the drop.
This is one of the most common stumbling blocks when working with drag and drop in Qt's model/view framework. The good news is that once you understand why it happens, the fix is straightforward.
In this tutorial, we'll walk through what causes the prohibited drop icon, how to fix it, and build a complete working example of a QTableView where you can drag and drop rows to reorder them.
Why the drop is prohibited
When you drag a row in a QTableView, Qt's drag and drop system goes through a series of checks before it will accept a drop. Setting properties on the view like setDragEnabled(True) and setDragDropMode(QAbstractItemView.InternalMove) is only half the story. The model behind the view also needs to cooperate.
Specifically, your QAbstractTableModel subclass needs to do three things:
- Return the right flags
- Implement
mimeData()andmimeTypes()— so Qt knows how to serialize the dragged data. - Implement
dropMimeData()— so Qt knows how to handle the incoming data when it's dropped.
Without all three, the view has no way to complete the drop, and it shows the prohibited icon instead.
Let's go through each of these in detail.
Getting the flags right
Here's the part that trips most people up. You might expect that you need Qt.ItemIsDropEnabled on every item so that rows can be dropped onto them. But for row reordering, the opposite is true.
When you drag a row and want to drop it between other rows (to reorder), you're dropping onto the root item of the model — the blank, invisible parent that contains all the top-level rows. You are not dropping onto an individual item. If individual items have Qt.ItemIsDropEnabled, Qt interprets the drop as "drop this data into that item" (like dropping a file into a folder), which isn't what you want for reordering.
So the correct setup is:
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.
- Items (valid indexes) should have
Qt.ItemIsDragEnabledbut notQt.ItemIsDropEnabled. - The root item (invalid index) should have
Qt.ItemIsDropEnabled.
In your flags() method, that looks like this:
def flags(self, index):
default_flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
if index.isValid():
# Items can be dragged, but NOT dropped onto
return default_flags | Qt.ItemIsDragEnabled
else:
# The root (empty area) accepts drops
return default_flags | Qt.ItemIsDropEnabled
When flags() is called with an invalid index, Qt is asking about the root item. By returning Qt.ItemIsDropEnabled only for the root, you're telling Qt: "drops are allowed between rows, but not onto rows." That's exactly what row reordering needs.
Implementing MIME data methods
Qt's drag and drop system uses MIME data to transfer information. Even for internal moves within the same table, the model needs to tell Qt how to package up the dragged row and how to unpack it at the drop site.
You need to implement three methods on your model:
mimeTypes()
This returns a list of MIME types your model supports. You can use any string as your custom MIME type — it just needs to be consistent between mimeData() and dropMimeData().
def mimeTypes(self):
return ["application/x-tableview-dragrow"]
mimeData()
This is called when a drag starts. It receives a list of QModelIndex objects for the items being dragged, and you need to package them into a QMimeData object.
def mimeData(self, indexes):
mime_data = QMimeData()
# Get the row numbers being dragged
rows = sorted(set(index.row() for index in indexes if index.isValid()))
# Encode the row numbers as bytes
data = QByteArray()
stream = QDataStream(data, QIODevice.WriteOnly)
for row in rows:
stream.writeInt32(row)
mime_data.setData("application/x-tableview-dragrow", data)
return mime_data
dropMimeData()
This is called when the drop happens. It receives the MIME data, the drop action, and the target row and column. For row reordering, you need to decode the source row from the MIME data, remove it from the old position, and insert it at the new position.
def dropMimeData(self, data, action, row, column, parent):
if action == Qt.IgnoreAction:
return True
if not data.hasFormat("application/x-tableview-dragrow"):
return False
# Decode the source row
encoded = data.data("application/x-tableview-dragrow")
stream = QDataStream(encoded, QIODevice.ReadOnly)
source_rows = []
while not stream.atEnd():
source_rows.append(stream.readInt32())
# If dropping on an item, use that item's row
if parent.isValid():
target_row = parent.row()
elif row >= 0:
target_row = row
else:
target_row = self.rowCount(QModelIndex())
# Move rows (handle one at a time for simplicity)
for source_row in reversed(source_rows):
if source_row < target_row:
target_row -= 1
item = self._data.pop(source_row)
self._data.insert(target_row, item)
self.layoutChanged.emit()
return True
The row parameter in dropMimeData() tells you where between existing rows the user dropped. A value of -1 with a valid parent means the drop was on a specific item (which we've disabled for regular items, but we handle it as a fallback). A value of -1 with an invalid parent means the drop was at the end of the list.
Implementing supportedDropActions()
You should also tell Qt which drop actions your model supports. For reordering, you want Qt.MoveAction:
def supportedDropActions(self):
return Qt.MoveAction
This ensures Qt uses move semantics (remove from source, insert at target) rather than copy semantics.
Complete working example
Here's a full, runnable example that puts it all together. You can copy this, run it, and immediately drag rows to reorder them:
import sys
from PySide6.QtCore import (
QAbstractTableModel,
QByteArray,
QDataStream,
QIODevice,
QMimeData,
QModelIndex,
Qt,
)
from PySide6.QtWidgets import (
QAbstractItemView,
QApplication,
QHeaderView,
QMainWindow,
QTableView,
QVBoxLayout,
QWidget,
)
class ReorderTableModel(QAbstractTableModel):
def __init__(self, data=None):
super().__init__()
self._data = data or []
def rowCount(self, parent=QModelIndex()):
return len(self._data)
def columnCount(self, parent=QModelIndex()):
return 1
def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return None
if role in (Qt.DisplayRole, Qt.EditRole):
return self._data[index.row()]
return None
def setData(self, index, value, role=Qt.EditRole):
if index.isValid() and role == Qt.EditRole:
self._data[index.row()] = value
self.dataChanged.emit(index, index, [role])
return True
return False
def flags(self, index):
default_flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
if index.isValid():
# Items can be dragged but NOT dropped onto
return default_flags | Qt.ItemIsDragEnabled
else:
# The root item accepts drops (between rows)
return default_flags | Qt.ItemIsDropEnabled
def supportedDropActions(self):
return Qt.MoveAction
def mimeTypes(self):
return ["application/x-tableview-dragrow"]
def mimeData(self, indexes):
mime_data = QMimeData()
rows = sorted(set(index.row() for index in indexes if index.isValid()))
data = QByteArray()
stream = QDataStream(data, QIODevice.WriteOnly)
for row in rows:
stream.writeInt32(row)
mime_data.setData("application/x-tableview-dragrow", data)
return mime_data
def dropMimeData(self, data, action, row, column, parent):
if action == Qt.IgnoreAction:
return True
if not data.hasFormat("application/x-tableview-dragrow"):
return False
# Decode the source row(s)
encoded = data.data("application/x-tableview-dragrow")
stream = QDataStream(encoded, QIODevice.ReadOnly)
source_rows = []
while not stream.atEnd():
source_rows.append(stream.readInt32())
# Determine target row
if parent.isValid():
target_row = parent.row()
elif row >= 0:
target_row = row
else:
target_row = self.rowCount()
# Move each dragged row to the target position
for source_row in sorted(source_rows, reverse=True):
item = self._data.pop(source_row)
# Adjust target if source was above it
if source_row < target_row:
target_row -= 1
self._data.insert(target_row, item)
self.layoutChanged.emit()
return True
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return "Item"
return None
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("QTableView Drag & Drop Reorder")
self.setMinimumSize(400, 350)
# Sample data
items = [
"First item",
"Second item",
"Third item",
"Fourth item",
"Fifth item",
"Sixth item",
"Seventh item",
]
# Set up the model
self.model = ReorderTableModel(items)
# Set up the table view
self.table = QTableView()
self.table.setModel(self.model)
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.table.setDragEnabled(True)
self.table.setAcceptDrops(True)
self.table.setDragDropMode(QAbstractItemView.InternalMove)
self.table.setDefaultDropAction(Qt.MoveAction)
self.table.setDragDropOverwriteMode(False)
self.table.setDropIndicatorShown(True)
# Stretch the column to fill the view
self.table.horizontalHeader().setSectionResizeMode(
QHeaderView.Stretch
)
# Layout
layout = QVBoxLayout()
layout.addWidget(self.table)
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
When you run this, you'll see a table with seven rows. Click and drag any row, and you'll see a drop indicator line appear between rows as you move the cursor. Release the mouse to drop the row in its new position — no prohibited icon in sight.

What each view property does
The view configuration includes several properties that all work together. Here's what each one does and why it matters:
| Property | Purpose |
|---|---|
setDragEnabled(True) |
Allows items to be dragged out of the view |
setAcceptDrops(True) |
Allows the view to receive drops |
setDragDropMode(InternalMove) |
Restricts drag and drop to within this view only, and uses move (not copy) semantics |
setDefaultDropAction(Qt.MoveAction) |
Sets move as the default action when dropping |
setDragDropOverwriteMode(False) |
Drops insert between rows instead of overwriting existing rows |
setDropIndicatorShown(True) |
Shows a visual line indicating where the drop will land |
Missing any one of these can cause subtle issues — from the prohibited icon to rows being duplicated instead of moved.
Common mistakes to watch for
Putting ItemIsDropEnabled on items instead of the root. This is the single most common cause of the prohibited icon. When items are drop-enabled, Qt tries to drop onto them (like placing something inside a folder). For reordering, only the root should be drop-enabled.
Forgetting to implement mimeData() and dropMimeData(). The base QAbstractTableModel doesn't provide working implementations of these methods. Without them, Qt has no way to serialize or deserialize the dragged data, so the drop silently fails.
Not emitting layoutChanged after modifying the data. After you move items around in your internal data list, the view needs to know the data has changed. Emitting layoutChanged tells the view to refresh everything. For more complex models, you might use beginMoveRows()/endMoveRows() instead, but layoutChanged is simpler and works well for this use case.
Using QStandardItemModel tips with QAbstractTableModel. Many drag and drop tutorials online are written for QStandardItemModel, which has built-in drag and drop support. If you're subclassing QAbstractTableModel, you need to implement the MIME methods yourself.
Wrapping up
Getting drag and drop to work with QTableView and a custom model requires coordination between the view's properties and the model's methods. The view needs to be told to allow dragging and accept drops, and the model needs to provide the right flags, serialize the dragged data, and handle the drop.
The counter-intuitive part — that individual items should not be drop-enabled for row reordering — is what catches most people. Once you understand that drops between rows are handled by the root item, everything else falls into place.
If you're new to Qt's model/view architecture, it's worth reading through the Model View Architecture tutorial to understand how models, views, and delegates work together. For working with tabular data in QTableView using numpy or pandas, see the QTableView with numpy and pandas tutorial. You can also learn more about drag and drop for custom widgets and signals and slots which underpin the layoutChanged signal used in this example.
Create GUI Applications with Python & Qt6 by Martin Fitzpatrick — (PySide6 Edition) The hands-on guide to making apps with Python — Save time and build better with this book. Over 15K copies sold.