Debugging ipywidgets in a Jupyter Notebook

Stop your error messages from being eaten

The ipywidgets library offers a big collection of standard widgets and supports some really powerful third-party widgets to like ipympl, ipyleaflet and more... So you can build some pretty sophisticated UI right in your Jupyter Notebook, then launch your app as a dashboard with voila or (ok, very soon!) create an interactive document with Curvenote.

When your UI starts getting bigger, developing and debugging can be tricky though β€” for numerous reasons β€” but the biggest thing is often lack of access to stdout and stderr from the various callback functions that you end up creating. Luckily there is a simple trick that helps solve this.

A broken exampleΒΆ

The following code has a problem, it will happily render my ipympl plot πŸ‘ but the pick event does not work. When I pick on the plot nothing happens at all, no feedback, no error message, nada.

import matplotlib.pyplot as plt
import numpy as np

%matplotlib widget

plt.ioff()
fig, ax = plt.subplots(1, 1, figsize=(4,4))
plt.ion()

ax.plot(np.sin(np.random.random(100)),picker=True, pickradius=2)

selection = { "line": None }

def onpick(e):
    m = e.mouseevent
    e.artist.set__color('#DC143C')    
    e.artist.set_zorder(10)
    e.artist.set_linestyle('-')
    e.artist.set_marker('o')
    e.artist.set_markersize(e.artist.get_markersize() + 1)
    selection["line"] = e.artist
    

cid = fig.canvas.mpl_connect('pick_event', onpick)

display(fig.canvas)

The problem is actually on the line πŸ‘‡

    e.artist.set__color('#DC143C')

Where there should be only one underscore, but the stderr generated in the callback function gets eaten. To solve this we need to set up a way for stderr to get out to the notebook interface. We can do this with ipywidgets, specifically the Output widget!

Debugging with an output widgetΒΆ

We need to do 3 things:

  • Create an instance of an Output widget at the top of our code
  • display(...) our debug Output widget somewhere (doesn't have to be in the same cell)
  • We add a conditional with context block at the top of our event handlers

After that, we get error messages showing up on our output display, and can also print(...) temporary debug messages there too while developing. To remove the output, we can remove the code that creates the instance and the conditional context blocks can stay in our callbacks.

And πŸ’₯ we now have stderr output!

%matplotlib widget

# create an output widget!
w_debug_output = Output()

plt.ioff()

fig, ax = plt.subplots(1, 1, figsize=(4,4))
plt.ion()
ax.plot(np.sin(np.random.random(100)),picker=True, pickradius=2)

def onpick(e):
    with w_debug_output if w_debug_output else nullcontext():
        m = e.mouseevent
        e.artist.set__color('#DC143C')    
        e.artist.set_zorder(10)
        e.artist.set_linestyle('-')
        e.artist.set_marker('o')
        e.artist.set_markersize(e.artist.get_markersize() + 1)
        selection["line"] = e.artist

cid = fig.canvas.mpl_connect('pick_event', onpick)

display(fig.canvas)
display(w_debug_output)

This makes it pretty easy to fix our error here and chance to catch other errors while developing with ipywidgets in a notebook. Also we can now print debug messages and they’ll also appear in the Output widget too.

Here's the result of this example code working below:

%matplotlib widget

# create an output widget!
w_debug_output = Output()

plt.ioff()

fig, ax = plt.subplots(1, 1, figsize=(4,4))
plt.ion()
ax.plot(np.sin(np.random.random(100)),picker=True, pickradius=2)

def onpick(e):
    with w_debug_output if w_debug_output else nullcontext():
        m = e.mouseevent
        e.artist.set_color('#DC143C')    
        e.artist.set_zorder(10)
        e.artist.set_linestyle('-')
        e.artist.set_marker('o')
        e.artist.set_markersize(e.artist.get_markersize() + 1)
        selection["line"] = e.artist

cid = fig.canvas.mpl_connect('pick_event', onpick)

display(fig.canvas)
display(w_debug_output)