Skip to content

Conversation

@larrybradley
Copy link
Contributor

PR summary

In 3.11.0.dev, AxesWidget.__del__ is called on partially constructed selectors when __init__ raises early (e.g., EllipseSelector(ax, foo="bar") with an invalid kwarg). Because __init__ never runs, attributes like _blit_background_id and canvas are not set, so __del__ gives Exception ignored while calling deallocator <function AxesWidget.__del__ at 0x108598b40>: and raises an AttributeError during garbage collection. This surfaces as PytestUnraisableExceptionWarning in tests in downstream packages.
See, e.g., https://github.com/astropy/regions/actions/runs/20730257507/job/59516157314

This bug was introduced in #30591.

Minimal example (also added as a regression test):

import matplotlib.pyplot as plt
from matplotlib.widgets import EllipseSelector
fig, ax = plt.subplots()
try:
    EllipseSelector(ax, foo="bar")
except TypeError:
    pass
plt.close(fig)

PR checklist

Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
@anntzer
Copy link
Contributor

anntzer commented Jan 12, 2026

Maybe instead of defining AxesWidget.__del__ at all it would be simpler to just register the necessary finalizer when the blitbackgroundid is first requested, i.e.

diff --git i/lib/matplotlib/widgets.py w/lib/matplotlib/widgets.py
index 5e87abe08d..9418794dcf 100644
--- i/lib/matplotlib/widgets.py
+++ w/lib/matplotlib/widgets.py
@@ -120,16 +120,6 @@ class AxesWidget(Widget):
         self._cids = []
         self._blit_background_id = None
 
-    def __del__(self):
-        blit_background_id = getattr(self, '_blit_background_id', None)
-        # __del__ may be called on a partially initialized object, e.g.,
-        # when __init__ raises. Therefore, we handle missing attributes
-        # gracefully.
-        if blit_background_id is not None:
-            canvas = getattr(self, 'canvas', None)
-            if canvas is not None:
-                canvas._release_blit_background_id(blit_background_id)
-
     canvas = property(
         lambda self: getattr(self.ax.get_figure(root=True), 'canvas', None)
     )
@@ -170,7 +160,10 @@ class AxesWidget(Widget):
         good enough for all existing widgets.
         """
         if self._blit_background_id is None:
-            self._blit_background_id = self.canvas._get_blit_background_id()
+            bbid = self.canvas._get_blit_background_id()
+            import weakref
+            weakref.finalize(self, self.canvas._release_blit_background_id, bbid)
+            self._blit_background_id = bbid
         self.canvas._blit_backgrounds[self._blit_background_id] = background
 
     def _load_blit_background(self):

?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants