I'm trying to update a QLabel in a loop (for example, cycling through random names), but the widget only shows the final value. The console prints each update in real time, but the UI freezes until the loop finishes. How do I make Qt widgets update in real time during a loop?
Qt applications are driven by an event loop. This is the engine that keeps your GUI responsive — it processes things like mouse clicks, keyboard input, and widget redraws, one at a time, in order.
When you call something like self.name_label.setText("Alice"), the text value on the widget changes immediately in memory, but the widget isn't redrawn right away. Instead, Qt places a "please redraw this widget" request onto the event queue. The event loop will get to that request and repaint the widget — but only once it's free to do so.
Here's where the problem comes in. If you connect a button click to a method that contains a long-running loop, the event loop has to wait for that entire method to finish before it can process anything else. That includes all those queued-up redraw requests. So even though you're calling setText() hundreds of times inside the loop, the widget never gets a chance to visually update until the very end.
The result: your loop runs, the console prints every update, but the UI appears frozen and then jumps straight to the final value.
Ways to fix it
There are a few approaches to solve this, each suited to different situations.
Use QApplication.processEvents() in the loop
The quickest fix is to force Qt to process pending events (including redraws) during your loop by calling QApplication.processEvents():
from PySide6.QtWidgets import QApplication
for i in range(100):
self.name_label.setText(f"Step {i}")
QApplication.processEvents()
This works, and you'll see the label update on each iteration. However, it's generally considered a workaround rather than a proper solution. It can lead to subtle bugs — for example, the user could click the button again while the loop is still running, or close the window mid-loop. For quick prototyping it's fine, but for anything more robust, one of the following approaches is better.
Run the loop in a thread
If you have a long-running task that can't easily be split into small pieces, running it in a separate thread keeps the UI responsive. Qt provides QThreadPool and QRunnable (or you can use signals to communicate results back to the main thread). This approach is well suited to tasks like file processing, network requests, or heavy computation.
For a full guide, see Multithreading PyQt6 applications with QThreadPool.
Use a QTimer for periodic updates (recommended for this kind of task)
For situations where your loop is really a series of quick, independent steps — like cycling through random names to build suspense — a QTimer is the cleanest solution. Instead of looping, you let the timer call your function at a regular interval. Between each call, the event loop is free to redraw widgets, handle input, and keep the UI feeling alive.
This is the recommended approach for the random-selection scenario described above, so let's walk through it in detail.
PyQt6 Crash Course by Martin Fitzpatrick — The important parts of PyQt6 in bite-size chunks
Using QTimer to update widgets in real time
The idea is simple: instead of one method that loops thousands of times, you write a method that does one step (pick a random name, update the label) and let a QTimer call that method repeatedly.
Setting up the timer
First, connect your button to a method that starts the timer, rather than one that runs a loop:
self.pick_btn.clicked.connect(self.start_selection)
In start_selection, create a QTimer that fires at a regular interval and connects to your single-step method:
def start_selection(self):
self.pick_timer = QTimer()
self.pick_timer.setInterval(100) # milliseconds between updates
self.pick_timer.timeout.connect(self.random_pick)
self.pick_timer.start()
# Stop after 5 seconds
QTimer.singleShot(5000, self.stop_selection)
The setInterval(100) means random_pick will be called every 100 milliseconds (10 times per second). You can adjust this to control how fast the names cycle.
QTimer.singleShot(5000, self.stop_selection) sets up a one-time timer that fires after 5000 milliseconds (5 seconds) and calls stop_selection to end the cycling.
The single-step method
Your random_pick method now handles just one selection:
def random_pick(self):
pick = random.choice(self.image_list)
self.name_label.setText(pick[1])
self.label.setPixmap(QPixmap(pick[0]))
self.current_selection = pick
Each time the timer fires, this method picks a random entry, updates the label and image, and stores the current selection.
Stopping the timer
When the single-shot timer fires after 5 seconds, it calls:
def stop_selection(self):
self.pick_timer.stop()
print("Selected:", self.current_selection[1])
At this point, the label shows whoever was last picked — your final selection.
Why this works
Between each call to random_pick, control returns to the event loop. That gives Qt the opportunity to process the redraw requests, so the label updates visually every 100 milliseconds. The UI stays responsive the entire time — the user can move the window, resize it, or interact with other widgets.
Complete working example
Here's a self-contained example you can copy and run. Since the original question used images from a folder, this simplified version uses just names (no images) so you can try it immediately without needing any files:
import sys
import random
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QPushButton,
QVBoxLayout,
QWidget,
)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Class Recitation")
self.students = [
"Alice Johnson",
"Bob Smith",
"Charlie Davis",
"Diana Lee",
"Ethan Brown",
"Fiona Garcia",
"George Wilson",
"Hannah Martinez",
"Isaac Anderson",
"Julia Thomas",
]
self.name_label = QLabel(self.students[0])
self.name_label.setStyleSheet("font-size: 32px; padding: 20px;")
self.pick_btn = QPushButton("Pick a Student")
self.pick_btn.clicked.connect(self.start_selection)
self.result_label = QLabel("")
self.result_label.setStyleSheet("font-size: 18px; color: green;")
layout = QVBoxLayout()
layout.addWidget(self.name_label)
layout.addWidget(self.pick_btn)
layout.addWidget(self.result_label)
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
self.current_selection = None
def start_selection(self):
# Disable the button while selection is running
self.pick_btn.setEnabled(False)
self.result_label.setText("")
# Create a repeating timer to cycle through names
self.pick_timer = QTimer()
self.pick_timer.setInterval(100)
self.pick_timer.timeout.connect(self.random_pick)
self.pick_timer.start()
# Stop after 3 seconds
QTimer.singleShot(3000, self.stop_selection)
def random_pick(self):
pick = random.choice(self.students)
self.name_label.setText(pick)
self.current_selection = pick
def stop_selection(self):
self.pick_timer.stop()
self.result_label.setText(f"Selected: {self.current_selection}")
self.pick_btn.setEnabled(True)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Run this and click "Pick a Student." You'll see the names cycling rapidly in the label for 3 seconds, then it stops on a final selection. The UI stays fully responsive throughout.
When to use a timer vs. a thread
A QTimer works well when your task can be split into small, independent steps — like this random selection example, where each step is quick and doesn't depend on the previous one.
If you have a task that genuinely needs to run continuously and can't be broken into small chunks — such as downloading a large file, running a complex calculation, or processing a big dataset — then a thread is the better choice. The rule of thumb: if each individual step is fast (a few milliseconds), a timer is usually sufficient. If the work itself is slow and can't be subdivided, use a thread.
For most UI animation and periodic-update scenarios, QTimer is the way to go.