QTableWidget Crashes When Updating Every 1 Millisecond

Why modifying widgets from background threads causes crashes and how to fix it with signals
Heads up! You've already completed this tutorial.

If you're building a PyQt6 application that receives data at high frequency — say via MQTT — and you're updating a QTableWidget from a background thread, you've probably already discovered the hard way that your application crashes with a cryptic "Python has stopped working" message.

This is one of the most common pitfalls in PyQt6 development, and it has a straightforward fix. In this article, we'll walk through what causes the crash and how to restructure your code using Qt signals so everything runs smoothly.

The Problem: Updating Widgets from a Background Thread

Here's a typical scenario. You have an MQTT client receiving messages every millisecond. Each message contains a row of data that needs to go into a QTableWidget. To avoid blocking the GUI, you've moved the data processing into a QRunnable or another thread, and you're using a multiprocessing.Queue to pass data between them.

Your update code might look something like this, running inside a QRunnable:

python
def UpdateTable(self):
    while True:
        if not self.Que.empty():
            self.table.setRowCount(1000)
            WholeData = self.Que.get()
            for item in WholeData:
                if len(item) > 0:
                    RawData = item.split(',')
                    self.table.insertRow(0)
                    for i in range(1, 4):
                        self.table.setItem(0, i - 1, QTableWidgetItem(str(RawData[i])))

At 100ms intervals this might seem to work fine. But crank it up to 1ms and the application crashes — sometimes immediately, sometimes after a few seconds.

The crash isn't about speed or the data itself. The problem is that you're modifying Qt widgets from a thread that isn't the main GUI thread. Qt widgets are not thread-safe. Any attempt to create, modify, or interact with widgets from outside the main thread can cause a segmentation fault, which shows up as that unhelpful "Python has stopped working" dialog.

This is a firm rule in Qt: all widget operations must happen on the main (GUI) thread. There are no exceptions and no workarounds — you have to respect this boundary.

The Fix: Use Signals to Send Data to the Main Thread

The solution is to keep your data collection in the background thread, but send the data back to the main thread using Qt's signal/slot mechanism. The main thread then handles the actual widget updates.

Here's how to restructure things.

Create GUI Applications with Python & Qt5 by Martin Fitzpatrick — (PyQt5 Edition) The hands-on guide to making apps with Python — Save time and build better with this book. Over 15K copies sold.

Get the book

Define a Signals Class

Qt signals need to be defined on a QObject subclass. Since QRunnable doesn't inherit from QObject, we create a small helper class:

python
from PyQt6.QtCore import QObject, pyqtSignal


class WorkerSignals(QObject):
    data = pyqtSignal(list)

This defines a signal called data that will carry a Python list when emitted.

Create a Worker That Emits Data

Your background worker collects data from the queue and emits it via the signal, rather than touching any widgets directly:

python
from PyQt6.QtCore import QRunnable


class DataWorker(QRunnable):
    def __init__(self, queue):
        super().__init__()
        self.queue = queue
        self.signals = WorkerSignals()

    def run(self):
        while True:
            if not self.queue.empty():
                whole_data = self.queue.get()
                self.signals.data.emit(whole_data)

Notice that the worker never touches a widget. It just emits the data. The actual table update happens elsewhere — on the main thread. For a deeper dive into using QRunnable and QThreadPool for background tasks, see our guide to multithreading PyQt6 applications.

Handle the Update on the Main Thread

In your main window class, define a method that receives the data and updates the table. Connect the worker's signal to this method:

python
from PyQt6.QtWidgets import QMainWindow, QTableWidget, QTableWidgetItem
from PyQt6.QtCore import QThreadPool


class MainWindow(QMainWindow):
    def __init__(self, queue):
        super().__init__()
        self.table = QTableWidget()
        self.table.setColumnCount(11)
        self.table.setRowCount(1000)
        self.setCentralWidget(self.table)

        self.threadpool = QThreadPool()

        worker = DataWorker(queue)
        worker.signals.data.connect(self.update_table)
        self.threadpool.start(worker)

    def update_table(self, whole_data):
        for item in whole_data:
            if len(item) == 0:
                continue

            raw_data = item.strip().split(',')
            if len(raw_data) < 4:
                continue

            self.table.insertRow(0)

            # Set the first 3 columns (indices 1, 2, 3 from raw data)
            for i in range(1, 4):
                self.table.setItem(
                    0, i - 1, QTableWidgetItem(str(raw_data[i]))
                )

            # Set remaining columns based on the count in raw_data[3]
            count = int(raw_data[3])
            for i in range(count):
                self.table.setItem(
                    0, i + 3, QTableWidgetItem(str(raw_data[i + 4]))
                )

            # Keep the table at 1000 rows max
            while self.table.rowCount() > 1000:
                self.table.removeRow(self.table.rowCount() - 1)

Because update_table is connected to the signal via connect(), Qt automatically ensures it runs on the main thread — even though the signal was emitted from a background thread. This is exactly what makes signals safe for cross-thread communication.

Complete Working Example

Here's a self-contained example that simulates incoming data using a multiprocessing.Queue and displays it in a QTableWidget. Instead of MQTT, a separate process generates fake data every few milliseconds:

python
import sys
import time
import multiprocessing
from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QTableWidget, QTableWidgetItem,
)
from PyQt6.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal


class WorkerSignals(QObject):
    """Defines signals available from a running worker."""
    data = pyqtSignal(list)


class DataWorker(QRunnable):
    """
    Worker that reads from a multiprocessing Queue
    and emits data via a signal.
    """
    def __init__(self, queue):
        super().__init__()
        self.queue = queue
        self.signals = WorkerSignals()

    def run(self):
        while True:
            if not self.queue.empty():
                whole_data = self.queue.get()
                self.signals.data.emit(whole_data)
            else:
                # Small sleep to avoid busy-waiting when the queue is empty.
                time.sleep(0.001)


class MainWindow(QMainWindow):
    MAX_ROWS = 1000

    def __init__(self, queue):
        super().__init__()
        self.setWindowTitle("QTableWidget with Threaded Updates")
        self.resize(800, 600)

        self.table = QTableWidget()
        self.table.setColumnCount(11)
        self.table.setRowCount(0)
        self.table.setHorizontalHeaderLabels(
            ["Timestamp", "Value", "Count"]
            + [f"D{i}" for i in range(8)]
        )
        self.setCentralWidget(self.table)

        self.threadpool = QThreadPool()

        worker = DataWorker(queue)
        worker.signals.data.connect(self.update_table)
        self.threadpool.start(worker)

    def update_table(self, whole_data):
        """
        Receives a list of comma-separated strings and inserts
        each one as a new row at the top of the table.
        """
        for item in whole_data:
            item = item.strip()
            if len(item) == 0:
                continue

            raw_data = item.split(',')
            if len(raw_data) < 4:
                continue

            # Insert a new row at the top
            self.table.insertRow(0)

            # Columns 0-2: timestamp, value, count
            for col in range(1, 4):
                if col < len(raw_data):
                    self.table.setItem(
                        0, col - 1, QTableWidgetItem(raw_data[col])
                    )

            # Remaining columns: data points
            count = int(raw_data[3])
            for i in range(count):
                idx = i + 4
                if idx < len(raw_data):
                    self.table.setItem(
                        0, i + 3, QTableWidgetItem(raw_data[idx])
                    )

        # Trim the table if it exceeds the maximum number of rows
        while self.table.rowCount() > self.MAX_ROWS:
            self.table.removeRow(self.table.rowCount() - 1)


def data_producer(queue):
    """
    Simulates an MQTT-like data source that puts messages
    into a multiprocessing Queue.
    """
    import random

    counter = 0
    while True:
        timestamp = int(time.time())
        value = random.randint(100, 999)
        num_points = 8
        points = [random.randint(1, 100) for _ in range(num_points)]
        points_str = ",".join(str(p) for p in points)

        line = f"c,{timestamp},{value},{num_points},{points_str}"
        queue.put([line])

        counter += 1
        time.sleep(0.005)  # 5ms interval — adjust to test


if __name__ == "__main__":
    data_queue = multiprocessing.Queue()

    # Start the data producer in a separate process
    producer = multiprocessing.Process(
        target=data_producer, args=(data_queue,), daemon=True
    )
    producer.start()

    app = QApplication(sys.argv)
    window = MainWindow(data_queue)
    window.show()
    sys.exit(app.exec())

Run this and you'll see rows appearing at the top of the table, with older rows being pushed down and eventually removed when the table exceeds 1000 rows.

A Note on 1ms Update Rates

Even with the threading issue fixed, updating a table widget every single millisecond is extremely aggressive. Each call to insertRow and setItem triggers layout recalculations and repaints. At 1000 updates per second, the GUI will struggle to keep up regardless of how you structure the code.

A more practical approach is to batch your updates. Instead of updating the table for every single message, collect messages over a short window (say 50–100ms) and then insert them all at once. You can use QTimer for this:

python
from PyQt6.QtCore import QTimer


class MainWindow(QMainWindow):
    MAX_ROWS = 1000

    def __init__(self, queue):
        super().__init__()
        self.setWindowTitle("QTableWidget with Batched Updates")
        self.resize(800, 600)

        self.table = QTableWidget()
        self.table.setColumnCount(11)
        self.table.setRowCount(0)
        self.table.setHorizontalHeaderLabels(
            ["Timestamp", "Value", "Count"]
            + [f"D{i}" for i in range(8)]
        )
        self.setCentralWidget(self.table)

        # Buffer to hold incoming data
        self.data_buffer = []

        self.threadpool = QThreadPool()

        worker = DataWorker(queue)
        worker.signals.data.connect(self.buffer_data)
        self.threadpool.start(worker)

        # Flush the buffer every 100ms
        self.update_timer = QTimer()
        self.update_timer.timeout.connect(self.flush_buffer)
        self.update_timer.start(100)

    def buffer_data(self, whole_data):
        """Store incoming data without touching the table yet."""
        self.data_buffer.extend(whole_data)

    def flush_buffer(self):
        """Insert all buffered rows into the table at once."""
        if not self.data_buffer:
            return

        # Temporarily disable updates to avoid repainting per-row
        self.table.setUpdatesEnabled(False)

        for item in self.data_buffer:
            item = item.strip()
            if len(item) == 0:
                continue

            raw_data = item.split(',')
            if len(raw_data) < 4:
                continue

            self.table.insertRow(0)

            for col in range(1, 4):
                if col < len(raw_data):
                    self.table.setItem(
                        0, col - 1, QTableWidgetItem(raw_data[col])
                    )

            count = int(raw_data[3])
            for i in range(count):
                idx = i + 4
                if idx < len(raw_data):
                    self.table.setItem(
                        0, i + 3, QTableWidgetItem(raw_data[idx])
                    )

        self.data_buffer.clear()

        # Trim excess rows
        while self.table.rowCount() > self.MAX_ROWS:
            self.table.removeRow(self.table.rowCount() - 1)

        self.table.setUpdatesEnabled(True)

The call to setUpdatesEnabled(False) before the batch and setUpdatesEnabled(True) afterward prevents the table from repainting after every single row insertion. This makes a huge difference when inserting many rows at once.

With batching, your MQTT client can still receive messages at whatever rate it needs to. The table just updates at a pace the GUI can handle comfortably.

Summary

When your QTableWidget crashes during fast updates, the cause is almost always the same: widgets are being modified from a background thread. Qt requires all widget interactions to happen on the main GUI thread.

The fix follows a simple pattern:

  1. Keep your data collection in a background thread (QRunnable, QThread, or a separate process).
  2. Send data to the main thread using a Qt signal.
  3. Connect that signal to a slot on your main window that performs the actual table update.
  4. For very high data rates, batch your updates using a QTimer and setUpdatesEnabled() to keep the GUI responsive.

This pattern works for any situation where background data needs to reach the GUI — not just QTableWidget, but any widget at all. For larger datasets where you need more control over how data is displayed, consider using QTableView with a custom model instead of QTableWidget, which gives you better performance and flexibility through Qt's Model/View architecture.

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

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

QTableWidget Crashes When Updating Every 1 Millisecond was written by Martin Fitzpatrick.

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt. Martin founded PythonGUIs to provide easy to follow GUI programming tutorials to the Python community. He has written a number of popular Python books on the subject.