Skip to content

main event loop

Using PyQt with QtAgg in Jupyterlab – III – a simple pattern for background threads

We can use PyQt to organize output of Machine Learning applications in Qt-windows outside of Jupyterlab notebooks on a Linux desktop. PyQt also provides us with an option to put long running Python code as ML training and evaluation runs into the background of Jupyterlab and redirect graphical and text output to elements of Qt windows. Moving long lasting Python jobs and ML algorithms to the background of Jupyterlab would have the advantages

  • that we could run short code segments in other notebook cells in the meantime
  • and keep up the responsiveness of PyQt and Qt-based Matplotlib windows on the desktop.

In the first two posts of this series

we saw that PyQt and its GUI-widgets work perfectly together with Matplotlib’s backend QtAgg. Matplotlib figures are actually handled as special Qt widgets by QtAgg. We also gathered some information on threads in relation to Python and (Py)Qt. We understood that all (Py)Qt-GUI-classes and widgets must be run in the main thread of Jupyterlab and that neither Qt-widgets nor Matplotlib functions are thread-safe.

As a consequence we need some thread-safe, serializing communication method between background threads and the main thread. Qt-signals are well suited for this purpose as they end up in the event queue of target threads with fitting slots and respective functions. The event queue and the related event loop in the main thread of a Qt application enforce the required serialization for our widgets and Matplotlib figures.

In this post I want to discuss a simple pattern of how to put workload for data production and refinement into the background and how to trigger the updates of graphical PyQt windows from there. The pattern is based on elements discussed in the 2nd post of this series.

Pattern for the interaction of background threads with Qt objects and widgets in the foreground

You may have read about various thread-related patterns as the producer/consumer pattern or the sender/receiver pattern.

It might appear that the main thread of a Jupyter notebook with an integrated main Qt event loop would be a natural direct consumer or receiver of data produced in the background for graphical updates. One could therefore be tempted to think of a private queue as an instrument of serialization which is read out periodically from an object in the main thread.

However, what we cannot do is to run a loop with a time.sleep(interval)-function in a notebook cell in the main thread for periodic queue handling. The reason is that we do not want to block other code cells or the main event loop in our Python notebook. While it is true that time.sleep() suspends a thread, so another thread can run (under the control of the GIL), the problem remains that within the original thread other code execution is blocked. (Actually, we could circumvent this problem by utilizing asyncio in a Jupyterlab notebook. But this is yet another pattern for parallelization. We will look at it in another post series.)

Now we have two options:

  1. We may instead use the particular queue which is already handled asynchronously in Jupyterlab – namely the event queue started by QtAgg. We know already that signals from secondary (QThread-based) threads are transformed into Qt-events. We can send relevant data together with such signals (events) from the background. They are placed in the main Qt event queue and dispatched by the main event loop to callbacks.
  2. If we instead like to use a private queue for data exchange between a background and the main thread we would still use signals and respective slot functions in the main thread. We access our queue via a slot’s callback and read-out only one or a few new entries from there and work with them.

I will use the second option for the exchange of larger data objects in another post in this series. The pattern discussed in this post will be build upon the first option. We will nevertheless employ our own queue for data exchange – but this time between two threads in the background.

Short running callbacks in the main thread

According to what we learned in the last post, we must take care of the following:

The code of a callback (as well as of event handlers) in the main thread should be very limited in time and execute as fast as possible to create GUI updates.

Otherwise we would block the execution of main event loop by our callback! And that would render other graphical objects on the desktop or in the notebook unresponsive. In addition it would also block running code in other cells.

This is really an important point: The integration of Qt with Jupyterlab via a hook for handling the the Qt main event loop seemingly in parallel to IPython kernel’s prompt loop is an important feature which guarantees responsiveness and which we do not want to spoil by our background-foreground-interaction.

This means that we should follow some rules to keep up responsiveness of Jupyterlab and QT-windows in the foreground, i.e. in the main thread of Jupyterlab:

  • All data which we want to display graphically in QT windows should already have been optimally prepared for plotting before the slot function uses them for QT widget or Matplotlib figure updates.
  • Slot functions (event handlers) should use the function Qwidget.Qapplication.process_events()
    to intermittently spin the event-loop for the update of widgets.
  • The updates of PyQt widgets should only periodically be triggered via signals from the background. The signals can carry the prepared data with them. (If we nevertheless use a private queue then the callback in the main thread should only perform one queue-access via get() per received signal.)
  • The period by which signals are emitted should be relatively big compared to the event-loop timing and the typical processing of other events.
  • We should separate raw data production in the background from periodic signal creation and the related data transfer.
  • Data production in the background should be organized along relatively small batches if huge amounts of data are to be processed.
  • We should try to circumvent parallelization limitations due to the GIL whenever possible by using C/C++-based modules.

In the end it is all about getting data and timing right. Fortunately, the amount of data which we produce during ML training runs, and which we want to display on some foreground window, is relatively small (per training epoch).

A simple pattern for background jobs and intermediate PyQt application updates

An object or function in a “worker thread” calculates and provides raw data with a certain production rate. These data are put in a queue. An object or function in a “receiver thread” periodically reads out the next entries in the queue. The receiver knows what to do with these data for plotting and presentation. It handles them, modifies them if necessary and creates signals (including some update data for PyQt widgets). It forwards these signals to a (graphical) application in the main foreground thread. There they end up as events in the Qt event queue. Qt handles respective (signal-) events by so called “slots“, i.e. by callbacks for the original signals. The PyQt- application there has a graphical Qt-window that visualizes (some of) the data.

Read More »Using PyQt with QtAgg in Jupyterlab – III – a simple pattern for background threads

Using PyQt with QtAgg in Jupyterlab – II – excursion on threads, signals and events

In the first post of this series on PyQt

Using PyQt with QtAgg in Jupyterlab – I – a first simple example

we have studied how to set up a PyQt application in a Jupyterlab notebook. The key to getting a seamless integration was to invoke the QtAgg-backend of Matplotlib. Otherwise we did not need to use any of Matplolib’s functionality. For our first PyQt test application we just used multiple nested Qt-widgets in a QMainWindow to create a simple, but interactive and instructive application in a Qt-window on the desktop.

So, PyQt works well with QtAgg and IPython. We just construct and show a QMainWindow; we need no explicit exec() command. An advantage of using PyQt is that we get moveable, resizable windows on our Linux desktop, outside the browser-bound Jupyterlab environment. Furthermore, PyQt offers a lot of widgets to build a full fledged graphical application interface with Python code.

But our funny PyQt example application still blocked the execution of code in other notebook cells! It just demonstrated why we need background threads when working with Jupyterlab and long running code segments. This would in particular be helpful when working with Machine Learning [ML] algorithms. Would it not be nice to put a task like the training of an ML algorithm into the background? And to redirect the intermediate output after training epochs into a textbox in a desktop window? While we work with other code in the same notebook?

The utilization of background threads is one of the main objectives of this post series. In the end we want to see a PyQt application (also hosting e.g. a Matplotlib figure canvas) which displays data that we created in a ML background job. All controlled from a Jupyterlab Python notebook.

To achieve this goal we need a general strategy how to split work between foreground and background tasks. The left graphics indicates in which direction we will move.

But first we need a toolbox and an overview over possible restrictions regarding Python threads and PyQt GUI widgets. In this post we, therefore, will look at relevant topics like concurrency limitations due to the Python GIL, thread support in Qt, Qt’s approach to inter-thread communication, signals and once again Qt’s main event loop. I will also discuss some obstacles which we have to overcome. All of this will give us sufficient knowledge to understand a concrete pattern for a workload distribution which I will present in the next post.

Level of this post: Advanced. Some general experience with asynchronous programming is helpful. But also beginners have a chance. For a ML-project I myself had to learn on rather short terms how one can handle thread interaction with Qt. In the last section of this post you will find links to comprehensive and helpful articles on the Internet. Regarding signals I especially recommend [1.1]. Regarding threads and the helpful effects of event loops for inter-thread communication I recommend to read [3.1] and [3.2] (!) in particular. Regarding the difference between signals and events I found the discussions in [2.1] helpful.

Read More »Using PyQt with QtAgg in Jupyterlab – II – excursion on threads, signals and events

Using PyQt with QtAgg in Jupyterlab – I – a first simple example

As my readers know I presently study how to work with background jobs for plot and information updates in Jupyterlab. The reason for this endeavor is that I want to organize my work with ML- training and long evaluation runs a bit differently in the future. In particular I want to have the freedom of working in other regions (other cells) of a Python3 notebook while the long running jobs do their work in the background. This includes that these other cells may also start major background jobs or run code in parallel to the background tasks.

After some experiments I think that for my particular purposes a combination of QTAgg, Matplotlib and PyQt provides the best solution.

What I like about PyQt is its very clear structure. And you can use QTextEdit-widgets as a kind of canvas for print output. And e special Figure-widget for Matplotlib. Using PyQt opens up for many new options to build nice application windows around your ML-algorithms on your Linux desktop. Which will be extremely valuable for presentations or customer end applications.

In this post series I will present a few test applications that cover a lot of topics around Qt-widgets, widget- communication, threads, signals and events. We will start with a simple foreground application. Then we turn to threads and signals between threads. Afterward we integrate Matplotlib and deepen our understanding of handling threads in PyQt applications for Matplotlib and print output.

Objectives of this post

This first post translates a question posed at Stackoverflow [1] into a more complex example. With it I want to show

  • how to set up your own PyQt application in a Jupyterlab (Python) environment and display it directly as a Qt-window on your desktop,
  • how to create a nested PyQt “sub-window”-structure with the help of a PyQt-window layout,
  • how to react to events,
  • how to give a central widget its own paintEvent and a painter for drawing graphical primitives,
  • how to “hide” a widget element,
  • how you control which of your graphical primitives is drawn and which not.

This application does not yet use background jobs. So, due to its present structure it will block running any Python code in other notebook cells while we have some action going on in the application’s PyQt-widgets.

We are going to change this during the course of this post series. For the time being my main objective is only to show that PyQt can be used at all with QtAgg in Jupyterlab notebooks.

My “application” is a bit stupid in so far as 2 main button only trigger a continuous dynamic painting of a red line from a sub-window’s center to the mouse pointer or to the corners of their surrounding sub-window areas. It is simple and trivial, but it nevertheless gave me some fun constructing it.

If you like, you may think of the red line a “laser beam” shooting at the mouse pointer or the frame corners. I will use this term below when I discuss the functionality. There is also a video link at the end of the post.

All of this has obviously nothing to do with ML, yet, but it gives you a feeling how we can compose PyQt applications in a Jupyterlab environment. In other posts of this blog we will use our acquired PyQt knowledge to put Keras based ML-machinery into our own graphical interfaces on the Linux desktop. I.e. outside the browser tab for Jupyterlab.

Level of this post: In parts advanced. Not because the example is so complicated. Nevertheless some background information on Qt-Window layouts and Widgets is required. Good introductions are given here. Details for all Qt-widgets that I use below can be found at

But note: For a Jupyterlab Python notebook with an QtAgg-backend you do not need the whole app.exec() stuff described in the tutorial examples.

Read More »Using PyQt with QtAgg in Jupyterlab – I – a first simple example