PyQt Gotchas

PyQt and Qt have a fundamental conflict: memory management. Python frees stuff when references go out of scope, while Qt, being based on C++, has no such automatic mechanism for free storage data (that is, stuff allocated with new). Qt obviates to this problem by using a hierarchy: when the parent is deleted, the children are deleted in cascade. Unfortunately, this can create a lot of hard-to-debug chaos in two cases:

Both cases will lead to access to freed memory, leading to undefined behavior. Let’s see some examples to make things clear.

Gotcha #1: Using WA_DeleteOnClose

Never use WA_DeleteOnClose.

  import sys
  from PyQt4 import QtGui, QtCore
  
  class Main(QtGui.QPushButton):
      def __init__(self, parent=None):
          QtGui.QPushButton.__init__(self, parent)
          self._label = QtGui.QLabel("hello")
          self._label.setAttribute(QtCore.Qt.WA_DeleteOnClose)   # Gotcha
          self.clicked.connect(self.showLabel)
  
      def showLabel(self, *args):
          self._label.show()
  
  if __name__ == "__main__":
      app = QtGui.QApplication(sys.argv)
      widget = Main()
      widget.show()
      sys.exit(app.exec_())

Launch the example, and press the button. A label will be shown. Close the label, and press the button again. The following error occurs: “RuntimeError: wrapped C/C++ object of type QLabel has been deleted”. The reason is that when the label is closed, the C++ object is deleted, but we are still holding a reference to it via the self._label python object. Any attempt to use the self._label object will involve a deleted backend, with bad consequences.

Gotcha #2: Using a parent that has local scope for a child that has broader scope

Launch the example, and press the button twice.

  import sys
  from PyQt4 import QtGui, QtCore
  
  class Main(QtGui.QPushButton):
       def __init__(self, parent=None):
          QtGui.QPushButton.__init__(self, parent)
          self._counter = 0
          self.clicked.connect(self.doThing)
  
      def doThing(self, *args):
          if self._counter == 0:
              widget = QtGui.QWidget()                          # Gotcha
              self._label = QtGui.QLabel("hello", widget)       # Gotcha
              self._label.show()
          elif self._counter == 1:
              self._label.setText("whatever")
  
          self._counter += 1
  
  if __name__ == "__main__":
       app = QtGui.QApplication(sys.argv)
  
      widget = Main()
      widget.show()
  
      sys.exit(app.exec_())

What happens here is that the parent of the label (widget) is local in scope, and its child is kept around as self._label. When widget goes out of scope in python, it is freed. This triggers a C++ delete of the children. However, we are still holding a reference to the python object representing one of the children (self._label), which is now referring to freed memory.

Gotcha #3: Using deleteLater

Launch the example, and press the button three times.

  import sys
  from PyQt4 import QtGui, QtCore
  
  class Main(QtGui.QPushButton):
      def __init__(self, parent=None):
          QtGui.QPushButton.__init__(self, parent)
          self._counter = 0
          self.clicked.connect(self.doThing)
  
      def doThing(self, *args):
          if self._counter == 0:
              self._label = QtGui.QLabel("hello")
              self._label.show()
          elif self._counter == 1:
              self._label.setText("whatever")
          elif self._counter == 2:
              self._label.deleteLater()
          elif self._counter > 2:
              self._label.setText("whatever")
  
          self._counter += 1
  
  if __name__ == "__main__":
      app = QtGui.QApplication(sys.argv)
  
      widget = Main()
      widget.show()
  
      sys.exit(app.exec_())

The problem here is due to deleteLater(). This routine schedules a deletion of a widget for later, but in doing so, only the C++ object is freed. The python object stays alive and again references freed memory.

Non-Gotcha: using local scope for child widgets

In this example, 2 subwidgets are added to the layout of a main widget. Access to the widgets are provided only by using the C++ API of Qt, ie. without setting a Python ‘self’ reference.

  import sys
  from PyQt4 import QtGui
  
  class A(QtGui.QLabel):
      def __init__(self, parent=None):
          QtGui.QLabel.__init__(self, parent)
  
  class B(QtGui.QLabel):
      def __init__(self, parent=None):
          QtGui.QLabel.__init__(self, parent)
  
  class Widget(QtGui.QWidget):
      def __init__(self):
          QtGui.QWidget.__init__(self)
  
          # Base layout
          layout = QtGui.QVBoxLayout(self)
  
          # Add widgets
          a = A(self)                          # Here
          b = B(self)                          # Here
  
          layout.addWidget(a)
          layout.addWidget(b)
   
      def a(self):
          return self.layout().itemAt(0).widget()
   
      def b(self):
          return self.layout().itemAt(1).widget()
    
  if __name__ == "__main__":
       app = QtGui.QApplication(sys.argv)
  
      widget = Widget()
      widget.show()
      print widget.a()
      print widget.b()
  
      sys.exit(app.exec_())

This is not a problem, because PyQt is clever enough to increment the pythonn reference count of the objects a and b. In this case, when the a and b objects go out of scope, the underlying C++ is not deleted as a cascade effect of the python decrement reference count.