how to not shoot yourself in the foot with Python and Qt

How to not shoot yourself in the foot with Python for Qt / PyQt

6 minutes read

In the last few weeks, I have been using Python and Qt, especially PyQt extensively.
During these weeks I have discovered a few ways how to shoot yourself in the foot accidentally.

In this article, you will learn which things you should watch out for when working on GUIs with Qt and Python and how to avoid the resulting problems.

The Application can't be stopped with Ctrl-C

The first thing you will notice when writing your first hello world application with PyQt is that you can't stop it anymore from the command-line with Ctrl-C.

Let's take a look at the following PyQt QML hello world application:

main.py

# -*- coding: utf-8 -*-
import sys

from PyQt5.QtGui import QGuiApplication
from PyQt5.QtQml import QQmlApplicationEngine

if __name__ == '__main__':
    app = QGuiApplication(sys.argv)

    engine = QQmlApplicationEngine()
    engine.load('./main.qml')

    sys.exit(app.exec_())

main.qml

import QtQuick 2.5
import QtQuick.Controls 2.0

ApplicationWindow {
    id: root
    visible: true

    Label {
        anchors.centerIn: parent
        text: qsTr("Hello World!")
    }
}

This code snippet works perfectly fine and when you execute it you will see the hello world window as expected.

PyQt hello-world

PyQt hello-world

However, the first time you try to stop the application from the console you will see that Ctrl-C has no effect and the application just keeps running.

$ python ./main.py
^C^C^C^C^C^C^C^C

Only when hitting the exit button in the application window, the application finally stops as we the expected KeyboardInterruptError:

 File "./main.py", line 13, in <module>
    sys.exit(app.exec_())
KeyboardInterrupt

Whats happening here?

Qt strongly builds on a concept called event loop. Such an event loop enables you to write parallel applications without multithreading. The concept of event loops is especially useful for applications where a long living process needs to handle interactions from a user or client. Therefore, you often will find event loops being used in GUI or web frameworks.

However, the pitfall here is that Qt is implemented in C++ and not in Python. When we execute app.exec_() we start the Qt/C++ event loop, which loops forever until it is stopped.

The problem here is that we don't have any Python events set up yet. So our event loop never churns the Python interpreter and so our signal delivered to the Python process is never processed. Therefore, our Python process never sees the signal until we hit the exit button of our Qt application window.

To circumvent this problem is very easy. We just need to set up a timer kicking off our event loop every few milliseconds.

# -*- coding: utf-8 -*-
import sys

from PyQt5.QtCore import QTimer
from PyQt5.QtGui import QGuiApplication
from PyQt5.QtQml import QQmlApplicationEngine

if __name__ == '__main__':
    app = QGuiApplication(sys.argv)

    engine = QQmlApplicationEngine()
    engine.load('./main.qml')

    timer = QTimer()
    timer.timeout.connect(lambda: None)
    timer.start(100)

    sys.exit(app.exec_())

All we added to the hello world application is the code to start to create and start a timer every 100 milliseconds. This way we can safely terminate our application with Ctrl-C from the command line.

NOTE: Don't forget to store the variable containing the timer instance somewhere or your timer instance will be garbage collected.

Your Python object lives longer than the QObject

Another "Shoot yourself in the foot" experience that you will potentially have using Python and Qt is related to memory management.

As we all know, Python supports automatic memory management, meaning a garbage collector looks for variables that aren't referenced anymore and frees memory. This usually works very well and it's probably one of the features that make Python way easier to work with than for example C++.

C++, on the other hand, allows more freedom when it comes to memory management. Freeing up resources is up to the programmer and in the responsibility of the C++ class. For this purpose, C++ objects have a destructor which is called when an object is destroyed.

However, for more complex applications memory management isn't as easy as it sounds. Who is responsible for cleaning up objects created outside of an instance and assigned to another object instance for example?

Qt and many other GUI related frameworks, therefore, come with their own memory management mechanism that is especially suitable for window-based graphical applications. The principle is pretty simple. Every object can have children and it is responsible for cleaning up its children. Let's say for example an application window has a child which is a button. When the window is closed, the window calls the deleteLater function of the button. This ensures that for example a complex GUI form is cleaned up in the right order.

Qt child parent relationship

Qt child parent relationship

Additionally, QObject's do not delete their children instantly, but instead, delegate the object deletion to the event loop. This ensures that for example objects created in other tasks are also cleaned up correctly.

To further complicate things, QtQuick also has a garbage collector similar to Python.

In most cases, you don't need to really care about Qt's memory management when working with Python and Qt. However, in some cases, it is possible that the Python object, or parts of it, lives longer than the Qt object.

Let's take a look at the following example.

long_living_object.py

# -*- coding: utf-8 -*-
import sys
import time
from threading import Timer

from PyQt5.QtCore import QTimer, QObject, pyqtSignal, pyqtProperty
from PyQt5.QtGui import QGuiApplication
from PyQt5.QtQml import QQmlApplicationEngine, qmlRegisterType


class GlobalTimer(object):
    def __init__(self, interval=1.0):
        self._registered = set()
        self._interval = interval
        self._timer = None
        self._start_timer()

    def register_callback(self, callback):
        self._registered.add(callback)

    def unregister_callback(self, callback):
        self._registered.remove(callback)

    def _start_timer(self):
        self._timer = Timer(self._interval, self._callback)
        self._timer.start()

    def _callback(self):
        for callback in self._registered:
            callback()
        self._start_timer()


Scheduler = GlobalTimer()


class Clock(QObject):

    timestampChanged = pyqtSignal(int)

    def __init__(self, parent=None):
        super(Clock, self).__init__(parent)
        self._timestamp = time.time()
        Scheduler.register_callback(self.tick)

    @pyqtProperty(int, notify=timestampChanged)
    def timestamp(self):
        return self._timestamp

    def tick(self):
        self._timestamp = time.time()
        self.timestampChanged.emit(self._timestamp)


if __name__ == '__main__':
    app = QGuiApplication(sys.argv)

    qmlRegisterType(Clock, 'mymodule', 1, 0, Clock.__name__)

    engine = QQmlApplicationEngine()
    engine.load('./long_living_object.qml')

    timer = QTimer()
    timer.timeout.connect(lambda: None)
    timer.start(100)

    sys.exit(app.exec_())

long_living_object.qml

import QtQuick 2.5
import QtQuick.Controls 2.0
import mymodule 1.0

ApplicationWindow {
    id: root
    visible: true
    width: 300
    height: 300

    Loader {
        id: clockLoader
        anchors.centerIn: parent
        active: showCheck.checked
        sourceComponent: clockComponent
    }

    Component {
        id: clockComponent
        Label {
            text: clock.timestamp

            Clock { id: clock }
        }
    }

    CheckBox {
        id: showCheck
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        text: qsTr("Show clock")
        checked: true
    }
}

In this example, we have GlobalTimer class which is instantiated as global Scheduler object. Inside the Clock class we register the tick function to as a callback.

In QML we use the Clock object inside a Loader component, so we can create and destroy it on demand. The running application looks as follows:

long_living_object window

long_living_object window

When we uncheck the showCheck checkbox, the Loader destroys our Label and Clock components. However, our global Scheduler object still keeps ticking and we soon see the following error in our console.

Exception in thread Thread-3:
Traceback (most recent call last):
  File "/usr/lib/python3.5/threading.py", line 914, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.5/threading.py", line 1180, in run
    self.function(*self.args, **self.kwargs)
  File "/how-to-not-shoot-yourself-in-the-foot/examples/long_living_object.py", line 30, in _callback
    callback()
  File "/how-to-not-shoot-yourself-in-the-foot/examples/long_living_object.py", line 52, in tick
    self.timestampChanged.emit(self._timestamp)
RuntimeError: wrapped C/C++ object of type Clock has been deleted

It turns out our Python object outlived the QObject because the global Scheduler object still has a reference to our tick instance function.

Of course, we shouldn't design our application this way in the first place, but sometimes we depend on external libraries that we can't control. For example, a middleware library which triggers callbacks when a message arrives.

So how do we fix this?

Well, the solution sounds trivial, just unregister our callback when the Python object is destroyed.

But wait - Python classes do not have a destructor (yes, there is __del__ method, but our Python object still lives, doesn't it?).

Luckily QObject has a signal that is triggered before destruction, the destroyed signal.

Sounds easy, let's try:

...
        self._timestamp = time.time()
        Scheduler.register_callback(self.tick)

        self.destroyed.connect(self._unregister)

    def _unregister(self):
        Scheduler.unregister_callback(self.tick)

    @pyqtProperty(int, notify=timestampChanged)
        def timestamp(self):
...

Okay, you quickly will see that it doesn't work. - Another hole in the foot.

For some reason, it doesn't work to connect instance functions directly to the destroyed signal. (If you know why, please let me know.)

However, it turns out that lambdas work fine:

self.destroyed.connect(lambda: self._unregister())

And finally, our application doesn't throw errors anymore.

NOTE: Qt signals and slots are disconnected automagically on QObject destruction -> therefore, if you can use Qt signals and slots instead of callback functions.

Alternatively, we could also use a weakref in this particular example.

Conclusion

Python and Qt are a great combo. However, it is very easy to shoot yourself in the foot.

To avoid such unwanted problems remember the following:

  • Always keep Python in the loop.
  • Use references to Python Qt objects carefully and cleanup correctly.
  • Use lambdas in QObject.destroyed.

I hope you have enjoyed reading this blog post and if so please subscribe and share it with your friends.

Your
Machine Koder

Spread the love

Comments 21

  1. I'll tell you what would make it less hard to shoot feet, if there was some decent ***ing documentation. Pyside had great documentation. Pyside2, not so much. And heres the kicker, its *just* different enough in undocumented ways that its borderline insanity inducing.

    1. Post
      Author
      1. I actually thin the problem was brokenness. I switched over to Riverbanks version, mostly just a case of switching imports and a very slightly different api for decorating signals etc, and it all just worked. That was a couple of weeks down the tube but I'm pretty happy now

    1. Post
      Author

      Yes. I use QML since the Symbian/Nokia days.

      QML is useful for any project that goes beyond the typical desktop application. I use it in all of my projects nowadays since I work on machine HMIs.

      If you are primarily targeting desktop computers, choosing widgets over QML might be a reasonable choice. However, the trend on the desktop is towards non-native applications thanks to the rise of the single page web apps, and so QML also becomes more relevant for desktop applications.

      Things like the QML designer and Qt Quick Controls are also getting more comfortable to use so that QML might become an interesting choice for novices as well.

  2. I wish there was a more comprehensive tutorial or book on PyQt5/PySide2, most of the stuff I can find online is geared towards the very basic use cases.

    1. Post
      Author

      I agree. There was someone on the PyQt mailing list recently working on a new PyQt5 book for Packt.

      I also was thinking about creating some video courses or writing a book myself, but I'm not committed yet. I'm wondering how many people are interested in learning Qt in the Python world?

      Machine Koder

        1. Post
          Author
      1. Chicken and egg ... build it and they will come. Qt on Python is extremely useful, but also a massive time sink. It needs more comprehensive teaching materials. A cookbook would also be nice. I'd buy it.

        1. Post
          Author
  3. Seems like the lambda is actually required because the way python handles method calls by passing the object as the first argument creates a bound method instead of a regular method.

    1. Post
      Author

      Good point. On the other hand, when the `QObject.destroyed` signal is emitted, the object and therefore, the bound method should still exist. Bound methods work perfectly fine with signals and slots in other places.

        1. Post
          Author
  4. super helpful. Trapping the destruction has been driving me nuts. I have many cases where __del__ is never called from PySide2 objects.

  5. Pingback: Python GUI list – Tsurubaso Agency

  6. Trying your solution for the keyboard interrupt does not work for me, I get the following output on the terminal:

    ^CTraceback (most recent call last):
    File "ui.py", line 285, in
    timer.timeout.connect(lambda: None)
    KeyboardInterrupt

    Any ideas why?

  7. I suspect that the lambda works, because it obfuscates for pyqt that this is a slot belonging to this object. Probably if you use the reference directly pyqt is able to detect that this is a slot that should be automatically disconnected at destruction, and does that automatically, before the destroyed signal is emitted. Just a theory though.

    1. Post
      Author

      Very interesting, that does indeed make sense. I checked if Qt/C++ has the same behavior, and indeed an ASSERT is thrown when a slot of the same object is called as a result of the destroyed signal. Similar to the PyQt case, this problem can be solved by introducing a lambda, so it looks like Qt signal/slot mechanism disconnects the signal/slots of the object before destroyed is called. Thanks a lot, this solves the mystery.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.