Have a button disconnect its own callback

I would like to have a matplotlib button that can only be used once. Ideally, I could do this by disconnecting the callback. However, there is an issue of timing in having a callback disconnect itself.

import matplotlib.pyplot as plt
from matplotlib.widgets import Button

fig, ax = plt.subplots()
donebutton = Button(ax, "Disconnect the button")
def donecallback(event):
    donebutton.disconnect(donecid)
    print("Disconnected")

donecid = donebutton.on_clicked(donecallback)

plt.show()

To disconnect the callback, I need its callback ID, donecid , which I obtain when I connect the callback. To connect the callback, I first must define it, donecallback . To define the callback, I must already know the CID. Therefore, I am stuck with a chicken-and-egg problem.

Trying to run the code above produces the following error:

Traceback (most recent call last):
  File "C:\Users\MyName\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0\LocalCache\local-packages\Python38\site-packages\matplotlib\cbook\__init__.py", line 196, in process
    func(*args, **kwargs)
  File "C:\Users\MyName\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0\LocalCache\local-packages\Python38\site-packages\matplotlib\widgets.py", line 210, in _release
    for cid, func in self.observers.items():
RuntimeError: dictionary changed size during iteration

I can try to fix the problem by making a button object so that the CID can be passed in as one of its properties, like this:

import matplotlib.pyplot as plt
from matplotlib.widgets import Button

def mycallback():
    print("Hello")

class SelfDisconnectingButton:
    
    def __init__(self, ax, callback, text):
        self.button = Button(ax, text)
        self.callback = callback
        self.cbcid = self.button.on_clicked(self.cbmethod)
        self.disconnectcid = self.button.on_clicked(self.disconnectmethod)
        self.pressedyet = False
    
    def cbmethod(self, event): # Callback method, used to get rid of event
        self.callback()
    
    def disconnectmethod(self, event):
        if self.pressedyet==False:
            self.pressedyet = True
            self.button.disconnect(self.cbcid)

fig, ax = plt.subplots()

buttonobj = SelfDisconnectingButton(ax, mycallback, "Press here")

plt.show()

…or this:

import matplotlib.pyplot as plt
from matplotlib.widgets import Button

class MyButton(Button):
    def on_clicked(self, *args, **kwargs):
        self.cb_id = super(MyButton, self).on_clicked(*args, **kwargs)
        return self.cb_id

    def disconnect(self):
        return super(MyButton, self).disconnect(self.cb_id)

fig, ax = plt.subplots()

donebutton = MyButton(ax, "Disconnect the button")
def donecallback(event):
    donebutton.disconnect()
    print("Disconnected")

donebutton.on_clicked(donecallback)

plt.show()

…but all of these produce the same dictionary error. How can I avoid this error, and what is the proper way to disconnect this callback after it is called? I would like to understand how to disconnect the callback properly. In principle I could avoid the problem by clearing the axes and then creating a new button with no callback, but that would be running away from the problem rather than solving it.

I think your only option is to add a 0 timer to remove the callback outside of its execution.

However, I think this is a bug that should be fixed; can you open a report on GitHub?

I’ve opened a pull request that should fix this.

Thank you for finding a solution! I will take this as an opportunity to learn about the process of submitting modifications to open-source code.