Reliable PyQt applications
published on Tuesday, November 8, 2016
A few months ago, I transitioned from wxWidgets to Qt as my primary framework for writing GUI applications in python. In hindsight, this change was long overdue and I'm now very satisfied with heart-felt increase in power that Qt's widgets bring over those of wx. For the most part, the change was unproblematic and things just work. However, there are a few rough edges where things could go a little smoother, but nothing that can't be fixed. For serious applications, I recommend handling these scenarios. A simple piece of boilerplate such as the code below will do fine. I did not see these techniques advertised in PyQt tutorials or example code, hence the post.
The question whether to use PyQt4, PyQt5 or PySide can have significant impact on which platforms, other applications, libraries and extensions your code may be compatible with. When writing anything but a small script, there will probably be that time when you wish you had based your code on another toolkit to be able to use some particular awesome library, snippet or maybe even just one specific method. What to do then? Maintain different branches for all the backends?
The answer is that it's much simpler than that and it doesn't have to be a static choice in your code. First, the PyQt4, PyQt5 and PySide APIs are not so different, so most things just work when replacing the import statements. Second, python modules are just regular objects, so the imported Qt modules can be stored in global variables which can then act polymorphically according to which module was imported. Third, python modules can execute arbitrary code, so you can pick the actual backend at runtime based on the environment or command line. This is best done in a dedicated .qt module which should be used to proxy all the Qt imports within your application.
Therefore, it may be enough to change your import statements to something like the following:
The .qt module in my application relies on qtconsole (which I use to embed a ipython shell in my application):
If you don't want to depend on such heavy gear, you can of course deliver your own loader code. Just take a peek at qtconsole's implementation, so you don't miss any important detail.
The thing that initially annoyed me the most when writing and testing my shiny (tt) new application, was that I could not quit it by pressing Ctrl-C in the console as I'm used to. This is particularly unpleasant if no window was opened or the Qt loop continues for some reason even after the last window was closed/hidden. Also, if you have another background thread running which is not properly managed by your main window, the application will live on even after the Qt event loop has stopped.
The issue is rooted in python's interrupt handling which only works while the interpreter is active, and the Qt event loop's ignorance about the python interpreter. The question has a more detailed explanation on the PyQt mailing list.
Given there are plenty of cases where Ctrl-C comes in handy, you can find solutions on stackoverflow. My personal implementation is an adaptation of the accepted answer that safe-guards against a few more edge-cases:
There is also an interesting solution based on signal.set_wakeup_fd, but I ruled this one out as not being cross-platform and introducing too much complexity.
If you're using PyQt5, you may have noticed that uncaught python exceptions cause the program to abort. This is probably not what you want in a GUI application where an exception that appears as the result of some dialog can very well be irrelevant for the rest of the program. In any case, you want to define a consistent behaviour across PyQt4 and PyQt5. This is achieved by explicitly setting an excepthook according to your needs: