When working with Qt slots and signals in PyQt6 you will discover the @pyqtSlot
decorator. This decorator is used to mark a Python function or method as a slot to which a Qt signal can be connected. However, as you can see in our signals and slots tutorials you don't have to use this. Any Python function or method can be used, normally, as a slot for a Qt signals. But elsewhere, in our threading tutorials we do use it.
What's going on here?
- Why do you sometimes use
@pyqtSlot
but usually not? - What happens when you omit it?
- Are there times when it is required?
What does the documentation say?
The PyQt6 documentation has a good explanation:
Although PyQt6 allows any Python callable to be used as a slot when connecting signals, it is sometimes necessary to explicitly mark a Python method as being a Qt slot and to provide a C++ signature for it. PyQt6 provides the
pyqtSlot()
function decorator to do this.Connecting a signal to a decorated Python method has the advantage of reducing the amount of memory used and is slightly faster.
From the above we see that:
- Any Python callable can be used as a slot when connecting signals.
- It is sometimes necessary to explicitly mark a Python method as being a Qt slot and to provide a C++ signature for it.
- There is a side-benefit in that marking a function or method with
pyqtSlot()
reduces the amount of memory used, and makes the slot faster.
When is it necessary?
Sometimes necessary is a bit vague. In practice the only situation where you need to use pyqtSlot
decorators is when working with threads. This is because of a difference in how signal connections are handled in decorated vs. undecorated slots.
Purchasing Power Parity
Developers in [[ country ]] get [[ discount.discount_pc ]]% OFF on all books & courses with code [[ discount.coupon_code ]]- If you decorate a method with
@pyqtSlot
then that slot is created as a native Qt slot, and behaves identically - If you don't decorate the method then PyQt6 will create a "proxy" object wrapper which provides a native slot to Qt
In normal use this is fine, aside from the performance impact (see below). But when working with threads, there is a complication: is the proxy object created on the GUI thread or on the runner thread. If it ends up on the wrong thread, this can lead to segmentation faults. Using the pyqtSlot
decorator side-steps this issue, because no proxy is created.
When updating my PyQt6 book I wondered -- is this still necessary?! -- and tested removing it from the examples. Many examples continue to work, but some failed. To be safe, use pyqtSlot
decorators on your QRunnable.run
methods.
What about performance?
The PyQt6 documentation notes that using native slots "has the advantage of reducing the amount of memory used and is slightly faster". But how much faster is it really, and does decorating slots actually save much memory?
We can test this directly by using this script from Oliver L Schoenborn. Updating for PyQt6 (replace PyQt5
with PyQt6
and it will work as-is) and running this we get the following results:
See the original results for PyQt5 for comparison.
First the results for the speed of emitting signals when connected to a decorated slot, vs non-decorated.
Raw slot mean, stddev: 0.578 0.024
Pyqt slot mean, stddev: 0.587 0.021
Percent gain with pyqtSlot: -2 %
The result shows pyqtSlot
as 2% slower, but this is negligible (the original data on PyQt5 also showed no difference). So, using pyqtSlot
will have no noticeable impact on the speed of signal handling in your applications.
Next are the results for establishing connections. This shows the speed, and memory usage of connecting to decorated vs. non-decorated slots.
PyQt/PySide 1:1 Coaching with Martin Fitzpatrick — Get one on one help with your Python GUI projects. Working together with you I'll identify issues and suggest fixes, from bugs and usability to architecture and maintainability.
Comparing mem and time required to create 10000000 connections, 1000 times
Measuring for 1000000 connections
# connects mem (bytes) time (sec)
Raw : 1000000 949186560 (905MB) 9.02
Pyqt Slot : 1000000 48500736 ( 46MB) 1.52
Ratios : 20 6
The results show that decorated slots are about 6x faster to connect to. This sounds like a big difference, but it would only be noticeable if an application was connecting a considerable number of signals. Based on these numbers, if you connected 100 signals the total execution time difference would be 0.9 ms vs 0.15 ms. This is negligible, not to mention imperceptible.
Perhaps more significant is that using raw connections uses 20x the memory of decorated connections. Again though, bear in mind that for a more realistic upper limit of connections (100) the actual difference here is 0.09MB vs 0.004MB.
The bottom line: don't expect any dramatic improvements in performance or memory usage from using slot decorators, unless you're working with insanely large numbers of signals or making regular connections you won't see any difference at all. That said, decorating your slots is an easy win if you need it.
Are there any other reasons to decorate a slot?
In Qt signals can be used to transmit more than one type of data by overloading signals and slots with different types.
For example, with the following code the my_slot_fn
will only receive signals which match the signature of two int
values.
@pyqtSlot(int, int)
def my_slot_fn(a, b):
pass
This is a legacy of Qt5 and not recommended in new code. In Qt6 all of these signals have been replaced with separate signals with distinct names for different types. I recommend you follow the same approach in your own code for the sake of simplicity.
Conclusion
The pyqtSlot
decorator can be used to mark Python functions or methods as Qt slots. This decorator is only required on slots which may be connected to across threads, for example the run
method of QRunnable
objects. For all other slots it can be omitted. There is a very small performance benefit to using it, which you may want to consider when your application makes a large number of signal/slot connections.
Packaging Python Applications with PyInstaller by Martin Fitzpatrick — This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.