Unhandled exceptions in PyQt5

Question:

Have a look at the following MWE.

import sys

from PyQt5.QtWidgets import QMainWindow, QPushButton, QApplication

class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.button = QPushButton('Bham!')
        self.setCentralWidget(self.button)
        self.button.clicked.connect(self.btnClicked)

    def btnClicked(self):
        print(sys.excepthook)
        raise Exception


#import traceback
#sys.excepthook = traceback.print_exception

if __name__ == '__main__':
    app = QApplication(sys.argv)
    mainWindow = MainWindow()
    mainWindow.show()
    app.exec_() 

I have a number of questions. I don’t know if they are all related (I guess so), so forgive me if they are not.

  1. When I run the above code from the terminal, all is fine. The program runs, if I click the button it prints the traceback and dies. If I run it inside an IDE (I tested Spyder and PyCharm), the traceback is not displayed. Any idea why? Essentially the same question was raised in other posts also on SO, here and here. Please don’t mark this as a duplicate of either of those; please read on.

  2. By adding the commented lines, the traceback is again displayed properly. However, they also have the nasty side effect that the app does no longer terminate on unhandled exceptions! I have no idea why this happens, as AFAIK excepthook only prints the traceback, it cannot prevent the program from exiting. At the moment it is called, it’s too late for rescue.

  3. Also, I don’t understand how Qt comes into play here, as exceptions that are not thrown inside a slot still crash the app as I would expect. No matter if I change excepthook or not, PyQt does not seem to override it as well (at least the print seems to suggest so).

FYI, I am using Python 3.5 with PyQt 5.6, and I am aware of the changes in the exception handling introduced in PyQt 5.5. If those are indeed the cause for the behaviour above, I would be glad hear some more detailed explanations.

Asked By: polwel

||

Answers:

When an exception happens inside a Qt slot, it’s C++ which called into your Python code. Since Qt/C++ know nothing about Python exceptions, you only have two possibilities:

  • Print the exception and return some default value to C++ (like 0, “” or NULL), possibly with unintended side effects. This is what PyQt < 5.5 does.
  • Print the exception and then call qFatal() or abort(), causing the application to immediately exit inside C++. That’s what PyQt >= 5.5 does, except when you have a custom excepthook set.

The reason Python still doesn’t terminate is probably because it can’t, as it’s inside some C++ code. The reason your IDE isn’t showing the stack is probably because it doesn’t deal with the abort() correctly – I’d suggest opening a bug against the IDE for that.

Answered By: The Compiler

Whilst @the-compiler’s answer is correct in explaining why it happens, I thought I might provide a workaround if you’d like these exceptions to be raised in a more pythony way.

I decorate any slots with this decorator, which catches any exceptions in the slot and saves them to a a global variable:

exc_info = None

def pycrash(func):
    """Decorator that quits the qt mainloop and stores sys.exc_info. We will then
    raise it outside the qt mainloop, this is a cleaner crash than Qt just aborting as
    it does if Python raises an exception during a callback."""

    def f(*args, **kwargs):
        global exc_info
        try:
            return func(*args, **kwargs)
        except:
            if exc_info is None # newer exceptions don't replace the first one
                exc_info = sys.exc_info()
                qapplication.exit()

    return f

Then just after my QApplication‘s exec(), I check the global variable and raise if there’s anything there:

qapplication.exec_()
if exc_info is not None:
    type, value, traceback = exc_info
    raise value.with_traceback(traceback)

This is not ideal because quitting the mainloop doesn’t stop other slots higher in the stack from still completing, and if the failed slot affects them, they might see some unexpected state. But IMHO it’s still much better than PyQt just aborting with no cleanup.

Answered By: Chris Billington