Tkinter, the after() Method, and Threading

Over the summer, I wrote up a post on my latest Raspberry Pi project, which is based around an 8 inch touchscreen and some Tkinter code.

It is heavily reliant on the after() method to periodically update the state of the various labels and buttons that comprise the UI elements. The main label cycles through RSS feeds, a shared Google calendar, and a NASA image of the day which involves some screenscraping. (I’m well aware of how bad an idea this is; in fact its reliability is something I’ll come back to in a second.) There is also a row of per-Hue light buttons that are constantly polling the bulbs’ states, and setting their colours in a meaningful way.

I continued refining the functionality after I posted the write-up and when I finally got to a point where I thought I was done, I noticed a problem. After a day or two, the buttons started to fall permanently out of sync with the bulb states. They displayed the wrong colour (for on / off / unreachable) but still worked to control the lights. For weeks, the cause was a complete mystery.

I think I’m starting to figure it out, starting with a few novice-level implementation errors. Despite the fact that I’ve been using Python for a very long time, it has generally been for fire-and-forget utility scripts. This code is fundamentally different in that it has to run indefinitely, which elevates the need for stability. At the same time, I introduced a few new moving parts (for me): using modules to try to make everything more readable and potentially reusable, and the use of the Python logging framework.

One embarrassingly simple mistake that I was also making was using a desktop shortcut to launch the GUI. While this made sense for the touchscreen, it meant I was losing anything that was being written to standard error. With the benefit of hindsight I am sure I could have redirected this using the logger but at the time, it appeared as if the functionality was failing silently no matter what logging statements I was using. But there was another aspect to this: the problem seems to have been with errors that I’d intuitively expect to be unrelated.

It appears that Tkinter is very sensitive to exceptions being thrown, and it’s worth digging into how this manifests in more detail. I have one module which is using an after() method call in a 15 second loop to poll the Hue hub for every bulb, using the Requests library. In another module, I have a randomised selection of content, all of which uses Requests in one way or another. One of these is for the NASA image of the day. The approach I use for this is pretty ridiculous: a search for a relative URL in the HTML, and then the construction of an absolute URL to haul down the image it refers to. Coming as a shock to absolutely no-one, this is occasionally unreliable.

What I wasn’t expecting was a Requests exception in one module affecting the use of the same library in another one.

While I have used threading in other languages, I haven’t in Python. I imagine that each of the after() calls I make is running in its own thread: I can’t see how this works any other way, otherwise the main event loop would be blocked. But the exception in one thread tearing other threads down? I’m not sure about this. There are plenty of hits on Google if you search for ‘Tkinter thread safety’ but, from what I can see, these get into discussions on what happens if you implement threads yourself as opposed to what’s going on under the hood.

I have – I think – eliminated most of the obviously problematic sources of errors by doing what I should have done in the first place for long-running code: surrounding the various calls to Requests, JSON parsing etc. in try / except clauses. This seems to have ironed out the stability issues. So far.

But I haven’t discounted the possibility that I’ve made a fundamental mistake with the approach I’ve taken…