2025, Nov 03 15:00

Why Your Matplotlib ipympl Clicks Don't Update Variables in JupyterLab (and How to Fix It)

Learn why Matplotlib ipympl clicks in JupyterLab don't update your list: callbacks run later. See code to show coordinates, manage globals, and disconnect.

When you wire up matplotlib for interactivity in JupyterLab (with ipympl), it’s tempting to treat the notebook like a linear script: run a cell, click twice, and expect the last print to reflect updated state. But event-driven code doesn’t behave that way. The callback executes later, after the cell is done, so anything printed at the end of the cell won’t reflect changes that happen during the clicks. That’s exactly why a global list of click coordinates looks empty even though each click visibly prints x and y.

Minimal example that looks right but prints an empty list

The following shows the common pattern where the coordinates are appended in the event handler, but the final print runs too early in the cell lifecycle.

%matplotlib ipympl

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit
from scipy import stats
import os
import ipywidgets as widgets

log_area = widgets.Output()
picks = []

# sample data used for the line plot
x_series = [1, 2, 3, 4, 5]
y_series = [1, 3, 2, 5, 1]

chart, axes = plt.subplots(figsize=(4, 4))
axes.plot(x_series, y_series, 'k-', linewidth=1.5)
axes.set_xlabel("Time (sec)")
axes.set_ylabel(r"Temperature ($\degree$ C)")
axes.tick_params(axis="both", direction='in', top=True, right=True)

plt.show()

@log_area.capture()
def on_press(evt):
    ix, iy = evt.xdata, evt.ydata
    print('x = ', ix, 'y = ', iy)

    global picks
    picks.append(ix)
    picks.append(iy)
    
    # Disconnect after 2 clicks
    if len(picks) == 4:
        chart.canvas.mpl_disconnect(hook)
    
    return picks

hook = chart.canvas.mpl_connect('button_press_event', on_press)
display(log_area)
print(picks)

The handler appends values to the global list and disconnects after two clicks. Yet the last line prints an empty list because it executes before any click happens. The notebook won’t re-run that line on your behalf after the clicks finish.

What’s really going on

In Jupyter (and GUIs in general), the code at the bottom of the cell executes immediately, while user interactions happen later via callbacks. The kernel doesn’t wait for mouse clicks the way a console waits for input(). That means printing a global list right after you register an event will reflect its initial state, not the post-click state. The list itself does update globally — you can confirm this by printing inside the callback — but the early print outside the handler isn’t “live.”

There’s another subtlety: returning a value from an event handler is ineffective here. The GUI machinery calls your function but doesn’t route the return value anywhere useful. If you need to use the values immediately, do it inside the handler. If you need them later, trigger that code manually once the clicks are done, for example by executing another cell after you’ve made the two clicks.

Working approach: show state as you click, and reuse it later

A practical pattern is to update a UI element during clicks and disconnect the event once you have both points. If you want to consume the collected coordinates right away, do it directly in the handler. If you prefer to process them afterwards, just run a later cell — by then the global state is populated. You can also add a button to trigger the follow-up code when you’re ready.

%matplotlib ipympl

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit
from scipy import stats
import os
import ipywidgets as widgets

log_area = widgets.Output()
picks = []

x_series = [1, 2, 3, 4, 5]
y_series = [1, 3, 2, 5, 1]

chart, axes = plt.subplots(figsize=(4, 4))
axes.plot(x_series, y_series, 'k-', linewidth=1.5)
axes.set_xlabel("Time (sec)")
axes.set_ylabel(r"Temperature ($\degree$ C)")
axes.tick_params(axis="both", direction='in', top=True, right=True)

plt.show()

@log_area.capture()
def on_press(evt):
    global picks
    
    ix, iy = evt.xdata, evt.ydata
    print('x = ', ix, 'y = ', iy)

    picks.append(ix)
    picks.append(iy)

    status_label.value = f'coord: {picks}'
    
    if len(picks) == 4:
        chart.canvas.mpl_disconnect(hook)
        # here you could use picks immediately if needed

hook = chart.canvas.mpl_connect('button_press_event', on_press)

status_label = widgets.Label(value=f'coord: {picks}')

display(status_label, log_area)

Now the state is visible as you click, and the event is properly disconnected after two clicks. If you need to consume picks elsewhere, run a subsequent cell after making your selections; the list is global and populated by then.

Why this matters

Notebook cells and GUI callbacks live on different timelines. Treating callbacks as if they were synchronous steps in a linear script leads to confusing symptoms: empty prints, ignored return values, and code that “sometimes works.” Understanding that Jupyter won’t wait for your clicks helps you structure interactive logic correctly: update UI during events, compute immediately inside the handler if needed, or trigger follow-up code after interactions.

Bottom line

Capture click coordinates inside the event handler, make state visible with a widget if helpful, and disconnect when you’ve collected enough points. Don’t rely on a trailing print in the same cell; it runs too early. If you need to use the coordinates later, execute the next cell after finishing the clicks. And remember: for GUI events, returning a value from the handler won’t help — act on the data directly in the callback or trigger the next step explicitly.

The article is based on a question from StackOverflow by Karla Wagner and an answer by furas.