2025, Dec 12 11:00
How to Safely Move a Widget Between QTableWidget Cells in PySide6 (No More Crashes)
Avoid crashes when moving cell widgets in PySide6 QTableWidget. Learn how removeCellWidget deletes index widgets and wrapping and reparenting keep moves safe.
Moving a widget between cells in a QTableWidget looks trivial: fetch the widget, remove it from the old cell, and set it into the new one. In practice, doing this the naive way often ends with a crash. Let’s walk through why it happens and how to implement a safe, predictable approach that works reliably in PySide6.
Problem demonstration
The following example creates a QLabel in cell (4, 0) and attempts to move it to (3, 0) when a button is clicked. The code looks reasonable, but it results in a process crash.
import sys
from PySide6.QtWidgets import (
QApplication, QMainWindow, QTableWidget, QLabel, QPushButton, QVBoxLayout, QWidget
)
class AppWindow(QMainWindow):
def __init__(self):
super().__init__()
self.grid = QTableWidget(5, 2) # 5 rows, 2 columns
self.grid.setHorizontalHeaderLabels(["Column 1", "Column 2"])
# Place a QLabel into cell (4, 0)
sample = QLabel("Test")
self.grid.setCellWidget(4, 0, sample)
# Button to trigger the move
self.trigger_btn = QPushButton("Move widget")
self.trigger_btn.clicked.connect(self.relocate_widget)
vbox = QVBoxLayout()
vbox.addWidget(self.grid)
vbox.addWidget(self.trigger_btn)
shell = QWidget()
shell.setLayout(vbox)
self.setCentralWidget(shell)
def relocate_widget(self):
"""Move the widget from cell (4, 0) to (3, 0)."""
unit = self.grid.cellWidget(4, 0)
self.grid.removeCellWidget(4, 0)
self.grid.setCellWidget(3, 0, unit)
unit.show()
# Debug
print(f"Cell (3,0) now has: {self.grid.cellWidget(3, 0)}")
print(f"Cell (4,0) now has: {self.grid.cellWidget(4, 0)}")
if __name__ == "__main__":
app = QApplication(sys.argv)
win = AppWindow()
win.show()
sys.exit(app.exec())
Typical console output confirms the widget appears to be in the new cell, but the process ends with exit code -1073741819 (0xC0000005).
What’s really going on
QTableWidget uses what are commonly called “index widgets” for cell-embedded widgets. Managing them with high-level APIs like setCellWidget() and removeCellWidget() has a crucial side effect: setting another widget for a cell — including setting no widget — deletes the previously associated widget on the Qt side. In this scenario, removeCellWidget() effectively behaves as a set operation with no widget and triggers deletion of the old widget. Once that happens, trying to reuse the same instance as if it were still valid leads to trouble.
There’s nothing you can do to stop that deletion once you use those APIs in the obvious way. The safe pattern is to avoid moving the “index widget” directly. Instead, place your actual control inside a container QWidget, and move that inner widget between containers by reparenting it before you call removeCellWidget() and setCellWidget().
Working approach
The solution is straightforward: store the real widget you care about inside a container and use a layout. The layout ensures proper geometry management, and reparenting the inner widget prevents it from being destroyed during the cell update.
import sys
from PySide6.QtWidgets import (
QApplication, QMainWindow, QTableWidget, QLabel, QPushButton, QVBoxLayout, QWidget, QHBoxLayout
)
class AppWindow(QMainWindow):
def __init__(self):
super().__init__()
self.grid = QTableWidget(5, 2) # 5 rows, 2 columns
self.grid.setHorizontalHeaderLabels(["Column 1", "Column 2"])
# Create a container for the label and add a layout for proper sizing
wrapper = QWidget()
hbox = QHBoxLayout(wrapper)
self.demo_lbl = QLabel("Test", wrapper)
hbox.addWidget(self.demo_lbl)
self.grid.setCellWidget(4, 0, wrapper)
# Button to trigger the move
self.trigger_btn = QPushButton("Move widget")
self.trigger_btn.clicked.connect(self.relocate_widget)
vbox = QVBoxLayout()
vbox.addWidget(self.grid)
vbox.addWidget(self.trigger_btn)
shell = QWidget()
shell.setLayout(vbox)
self.setCentralWidget(shell)
def relocate_widget(self):
"""Move the label from cell (4, 0) to (3, 0) safely."""
new_wrapper = QWidget()
new_layout = QHBoxLayout(new_wrapper)
new_layout.addWidget(self.demo_lbl)
self.grid.setCellWidget(3, 0, new_wrapper)
self.grid.removeCellWidget(4, 0)
# Debug
print(f"Cell (3,0) now has: {self.grid.cellWidget(3, 0)}")
print(f"Cell (4,0) now has: {self.grid.cellWidget(4, 0)}")
if __name__ == "__main__":
app = QApplication(sys.argv)
win = AppWindow()
win.show()
sys.exit(app.exec())
With this pattern, the inner QLabel is reparented into a new container before the old cell is cleared. The view replaces the old “index widget” safely, and your label survives the operation.
Why this matters
High-level item views like QTableWidget are convenient, but they have ownership semantics that are easy to overlook. When you assign a widget to a cell, the view manages its lifetime. Clearing or replacing that cell deletes the previous instance. Understanding this prevents hard-to-diagnose crashes and makes your widget handling robust.
If you need to move items conceptually rather than just visually, consider model-driven operations. For use cases that involve moving rows, a QAbstractItemModel subclass allows proper row moves using beginMoveRows() and endMoveRows(). If your goal is to reorder how rows appear without touching the data, sometimes simply moving the section in the vertical header with moveSection() is acceptable.
Takeaways
When embedding controls into QTableWidget cells, treat them as “index widgets” owned by the view. Don’t try to lift and place the same widget instance directly between cells. Instead, wrap the control in a container and reparent the inner widget before clearing or setting the cell widget. Use a layout in the container so the embedded control is resized correctly as the cell changes. For structural moves, rely on model operations or header section reordering when that fits the task.