In PyQt version 5.15.0 and PySide 6.2.0, the .start()
method of QThreadPool
was extended to take a Python function, a Python method, or a PyQt/PySide slot, besides taking only a QRunnable
object. This simplifies running Python code in the background, avoiding the hassle of creating a QRunnable
object for each task.
For more information about creating a QRunnable
object for multithreading, see the multithreading tutorial.
The .start()
method schedules the execution of a function/method/slot on a separate thread using QThreadPool
, so it avoids blocking the main GUI thread of your app. Therefore, if you have one or more long-running tasks that need to be completed or be running in the background, pass them to .start()
and be done.
We'll build a simple demo app that simulates a long-running task to show how .start()
can move a user-defined Python function/method or a PyQt/PySide slot onto a separate thread.
But first, let’s begin with a flawed approach.
Blocking the GUI
Our demo app is a sheep counter that counts upwards from 1. While this is happening, you can press a button to pick a sheep. And since picking a sheep is hard, it takes some time to complete.
This is how our demo app looks like.
Create GUI Applications with Python & Qt6 by Martin Fitzpatrick — (PyQt6 Edition) The hands-on guide to making apps with Python — Over 10,000 copies sold!
Make sure you're using PyQt 5.15.0+ or PySide 6.2.0+; otherwise, the demo app won’t work for you.
- PySide6
- PyQt6
- PyQt5
import time
from PySide6.QtCore import Slot, QTimer
from PySide6.QtWidgets import (
QLabel,
QWidget,
QMainWindow,
QPushButton,
QVBoxLayout,
QApplication,
)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFixedSize(250, 100)
self.setWindowTitle("Sheep Picker")
self.sheep_number = 1
self.timer = QTimer()
self.picked_sheep_label = QLabel()
self.counted_sheep_label = QLabel()
self.layout = QVBoxLayout()
self.main_widget = QWidget()
self.pick_sheep_button = QPushButton("Pick a sheep!")
self.layout.addWidget(self.counted_sheep_label)
self.layout.addWidget(self.pick_sheep_button)
self.layout.addWidget(self.picked_sheep_label)
self.main_widget.setLayout(self.layout)
self.setCentralWidget(self.main_widget)
self.timer.timeout.connect(self.count_sheep)
self.pick_sheep_button.pressed.connect(self.pick_sheep)
self.timer.start()
@Slot()
def count_sheep(self):
self.sheep_number += 1
self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.")
@Slot()
def pick_sheep(self):
self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!")
time.sleep(5) # This function represents a long-running task!
if __name__ == "__main__":
app = QApplication([])
main_window = MainWindow()
main_window.show()
app.exec()
import time
from PyQt6.QtCore import pyqtSlot, QTimer
from PyQt6.QtWidgets import (
QLabel,
QWidget,
QMainWindow,
QPushButton,
QVBoxLayout,
QApplication,
)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFixedSize(250, 100)
self.setWindowTitle("Sheep Picker")
self.sheep_number = 1
self.timer = QTimer()
self.picked_sheep_label = QLabel()
self.counted_sheep_label = QLabel()
self.layout = QVBoxLayout()
self.main_widget = QWidget()
self.pick_sheep_button = QPushButton("Pick a sheep!")
self.layout.addWidget(self.counted_sheep_label)
self.layout.addWidget(self.pick_sheep_button)
self.layout.addWidget(self.picked_sheep_label)
self.main_widget.setLayout(self.layout)
self.setCentralWidget(self.main_widget)
self.timer.timeout.connect(self.count_sheep)
self.pick_sheep_button.pressed.connect(self.pick_sheep)
self.timer.start()
@pyqtSlot()
def count_sheep(self):
self.sheep_number += 1
self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.")
@pyqtSlot()
def pick_sheep(self):
self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!")
time.sleep(5) # This function represents a long-running task!
if __name__ == "__main__":
app = QApplication([])
main_window = MainWindow()
main_window.show()
app.exec()
import time
from PyQt5.QtCore import pyqtSlot, QTimer
from PyQt5.QtWidgets import (
QLabel,
QWidget,
QMainWindow,
QPushButton,
QVBoxLayout,
QApplication,
)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFixedSize(250, 100)
self.setWindowTitle("Sheep Picker")
self.sheep_number = 1
self.timer = QTimer()
self.picked_sheep_label = QLabel()
self.counted_sheep_label = QLabel()
self.layout = QVBoxLayout()
self.main_widget = QWidget()
self.pick_sheep_button = QPushButton("Pick a sheep!")
self.layout.addWidget(self.counted_sheep_label)
self.layout.addWidget(self.pick_sheep_button)
self.layout.addWidget(self.picked_sheep_label)
self.main_widget.setLayout(self.layout)
self.setCentralWidget(self.main_widget)
self.timer.timeout.connect(self.count_sheep)
self.pick_sheep_button.pressed.connect(self.pick_sheep)
self.timer.start()
@pyqtSlot()
def count_sheep(self):
self.sheep_number += 1
self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.")
@pyqtSlot()
def pick_sheep(self):
self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!")
time.sleep(5) # This function represents a long-running task!
if __name__ == "__main__":
app = QApplication([])
main_window = MainWindow()
main_window.show()
app.exec()
When you run the demo app and press the Pick a sheep! button, you’ll notice that for 5 seconds, the GUI is completely unresponsive. That's not good.
The delay in GUI responsiveness comes from the line time.sleep(5)
which pauses execution of Python code for 5 seconds. This was added to simulate a long-running task. We can, however, improve that by threading, as you’ll see later on.
Feel free to experiment by increasing the length of the delay – pass a number greater than 5 to .sleep()
– and you may notice that your operating system starts complaining about the demo app not responding.
Run a task on a separate thread
So, how can we improve the responsiveness of our demo app? This is where the extended .start()
method of QThreadPool
comes in!
First, we need to import QThreadPool
, so let’s do that.
- PySide6
- PyQt6
- PyQt5
from PySide6.QtCore import QThreadPool
from PyQt6.QtCore import QThreadPool
from PyQt5.QtCore import QThreadPool
Next, we need to create a QThreadPool
instance. Let’s add
self.thread_manager = QThreadPool()
to the __init__
block of the MainWindow
class.
Now, let’s create a pick_sheep_safely()
slot. It will use the .start()
method to call the long-running pick_sheep()
slot and move it from the main GUI thread onto a separate thread.
- PySide
- PyQt
@Slot()
def pick_sheep_safely(self):
self.thread_manager.start(self.pick_sheep) # This is where the magic happens!
@pyqtSlot()
def pick_sheep_safely(self):
self.thread_manager.start(self.pick_sheep) # This is where the magic happens!
Also, make sure that you connect the pick_sheep_safely()
slot with the pressed
signal of self.pick_sheep_button
. So, in the __init__
block of the MainWindow
class, you should have
self.pick_sheep_button.pressed.connect(self.pick_sheep_safely)
And if you followed along, the code of our improved demo app should now be:
- PySide6
- PyQt6
- PyQt5
import time
from PySide6.QtCore import Slot, QThreadPool, QTimer
from PySide6.QtWidgets import (
QLabel,
QWidget,
QMainWindow,
QPushButton,
QVBoxLayout,
QApplication,
)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFixedSize(250, 100)
self.setWindowTitle("Sheep Picker")
self.sheep_number = 1
self.timer = QTimer()
self.picked_sheep_label = QLabel()
self.counted_sheep_label = QLabel()
self.layout = QVBoxLayout()
self.main_widget = QWidget()
self.thread_manager = QThreadPool()
self.pick_sheep_button = QPushButton("Pick a sheep!")
self.layout.addWidget(self.counted_sheep_label)
self.layout.addWidget(self.pick_sheep_button)
self.layout.addWidget(self.picked_sheep_label)
self.main_widget.setLayout(self.layout)
self.setCentralWidget(self.main_widget)
self.timer.timeout.connect(self.count_sheep)
self.pick_sheep_button.pressed.connect(self.pick_sheep_safely)
self.timer.start()
@Slot()
def count_sheep(self):
self.sheep_number += 1
self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.")
@Slot()
def pick_sheep(self):
self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!")
time.sleep(5) # This function doesn't affect GUI responsiveness anymore...
@Slot()
def pick_sheep_safely(self):
self.thread_manager.start(self.pick_sheep) # ...since .start() is used!
if __name__ == "__main__":
app = QApplication([])
main_window = MainWindow()
main_window.show()
app.exec()
import time
from PyQt6.QtCore import pyqtSlot, QThreadPool, QTimer
from PyQt6.QtWidgets import (
QLabel,
QWidget,
QMainWindow,
QPushButton,
QVBoxLayout,
QApplication,
)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFixedSize(250, 100)
self.setWindowTitle("Sheep Picker")
self.sheep_number = 1
self.timer = QTimer()
self.picked_sheep_label = QLabel()
self.counted_sheep_label = QLabel()
self.layout = QVBoxLayout()
self.main_widget = QWidget()
self.thread_manager = QThreadPool()
self.pick_sheep_button = QPushButton("Pick a sheep!")
self.layout.addWidget(self.counted_sheep_label)
self.layout.addWidget(self.pick_sheep_button)
self.layout.addWidget(self.picked_sheep_label)
self.main_widget.setLayout(self.layout)
self.setCentralWidget(self.main_widget)
self.timer.timeout.connect(self.count_sheep)
self.pick_sheep_button.pressed.connect(self.pick_sheep_safely)
self.timer.start()
@pyqtSlot()
def count_sheep(self):
self.sheep_number += 1
self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.")
@pyqtSlot()
def pick_sheep(self):
self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!")
time.sleep(5) # This function doesn't affect GUI responsiveness anymore...
@pyqtSlot()
def pick_sheep_safely(self):
self.thread_manager.start(self.pick_sheep) # ...since .start() is used!
if __name__ == "__main__":
app = QApplication([])
main_window = MainWindow()
main_window.show()
app.exec()
import time
from PyQt5.QtCore import pyqtSlot, QThreadPool, QTimer
from PyQt5.QtWidgets import (
QLabel,
QWidget,
QMainWindow,
QPushButton,
QVBoxLayout,
QApplication,
)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFixedSize(250, 100)
self.setWindowTitle("Sheep Picker")
self.sheep_number = 1
self.timer = QTimer()
self.picked_sheep_label = QLabel()
self.counted_sheep_label = QLabel()
self.layout = QVBoxLayout()
self.main_widget = QWidget()
self.thread_manager = QThreadPool()
self.pick_sheep_button = QPushButton("Pick a sheep!")
self.layout.addWidget(self.counted_sheep_label)
self.layout.addWidget(self.pick_sheep_button)
self.layout.addWidget(self.picked_sheep_label)
self.main_widget.setLayout(self.layout)
self.setCentralWidget(self.main_widget)
self.timer.timeout.connect(self.count_sheep)
self.pick_sheep_button.pressed.connect(self.pick_sheep_safely)
self.timer.start()
@pyqtSlot()
def count_sheep(self):
self.sheep_number += 1
self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.")
@pyqtSlot()
def pick_sheep(self):
self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!")
time.sleep(5) # This function doesn't affect GUI responsiveness anymore...
@pyqtSlot()
def pick_sheep_safely(self):
self.thread_manager.start(self.pick_sheep) # ...since .start() is used!
if __name__ == "__main__":
app = QApplication([])
main_window = MainWindow()
main_window.show()
app.exec()
When you press the Pick a sheep! button now, the pick_sheep()
slot is executed on a separate thread and no longer blocks the main GUI thread. The sheep counting goes on, and the GUI remains responsive – even though our demo app still has to complete a long-running task in the background.
Try increasing the length of the delay now – for example, time.sleep(10)
– and notice that it doesn’t affect the GUI anymore.
Conclusion
And that’s it! I hope you’ll find the extended .start()
method of QThreadPool
helpful in any of your PyQt/PySide GUI apps that have long-running tasks to be executed in the background.