From 5bc69c99fce5fb54a26034fbe8016a6238b60cd3 Mon Sep 17 00:00:00 2001 From: Larry Bradley Date: Mon, 5 Jan 2026 18:35:41 -0500 Subject: [PATCH 1/5] FIX: Handle AxesWidget cleanup after failed init --- lib/matplotlib/tests/test_widgets.py | 8 ++++++++ lib/matplotlib/widgets.py | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 9eebf165e71f..7576b5d2c988 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -517,6 +517,14 @@ def test_rectangle_resize_square_center_aspect(ax, use_data_coordinates): 46.25, 133.75]) +def test_axeswidget_del_on_failed_init(): + fig, ax = plt.subplots() + try: + widgets.Button(ax, foo='bar') + except TypeError: + pass + + def test_ellipse(ax): """For ellipse, test out the key modifiers""" tool = widgets.EllipseSelector(ax, grab_range=10, interactive=True) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 79b2e3b7651c..7fa14bd304fe 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -121,8 +121,11 @@ def __init__(self, ax): self._blit_background_id = None def __del__(self): - if self._blit_background_id is not None: - self.canvas._release_blit_background_id(self._blit_background_id) + blit_background_id = getattr(self, '_blit_background_id', None) + 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) From 4963e016b984dadcbe7337c0f271da55b4b446ff Mon Sep 17 00:00:00 2001 From: Larry Bradley Date: Wed, 7 Jan 2026 19:13:04 -0500 Subject: [PATCH 2/5] Apply code review suggestions --- lib/matplotlib/tests/test_widgets.py | 4 ++++ lib/matplotlib/widgets.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 7576b5d2c988..a005eff448ce 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -518,6 +518,10 @@ def test_rectangle_resize_square_center_aspect(ax, use_data_coordinates): def test_axeswidget_del_on_failed_init(): + """Test that __del__ works correctly when __init__ fails.""" + # __del__ is called implicitly as part of a failed initialization + # here, which could create an unraisable exception in __del__. + # Pytest would fail the test if such an exception occurred. fig, ax = plt.subplots() try: widgets.Button(ax, foo='bar') diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 7fa14bd304fe..5e87abe08dc4 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -122,6 +122,9 @@ def __init__(self, ax): 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: From adfd6bbe1a63c59daf411f97e1efa79d99d3e45f Mon Sep 17 00:00:00 2001 From: Larry Bradley Date: Thu, 8 Jan 2026 12:04:30 -0500 Subject: [PATCH 3/5] Update lib/matplotlib/tests/test_widgets.py Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/tests/test_widgets.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index a005eff448ce..9a31c693ac3d 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -523,10 +523,8 @@ def test_axeswidget_del_on_failed_init(): # here, which could create an unraisable exception in __del__. # Pytest would fail the test if such an exception occurred. fig, ax = plt.subplots() - try: - widgets.Button(ax, foo='bar') - except TypeError: - pass + with pytest.raises(TypeError, match="unexpected keyword argument 'undefined'"): + widgets.Button(ax, undefined='bar') def test_ellipse(ax): From b60db73f4de57246e855fb7f676217be4109f084 Mon Sep 17 00:00:00 2001 From: Larry Bradley Date: Fri, 23 Jan 2026 16:22:53 -0500 Subject: [PATCH 4/5] Remove __del__ method --- lib/matplotlib/tests/test_widgets.py | 7 ++++--- lib/matplotlib/widgets.py | 15 ++++----------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 9a31c693ac3d..2f6c91b879a7 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -518,9 +518,10 @@ def test_rectangle_resize_square_center_aspect(ax, use_data_coordinates): def test_axeswidget_del_on_failed_init(): - """Test that __del__ works correctly when __init__ fails.""" - # __del__ is called implicitly as part of a failed initialization - # here, which could create an unraisable exception in __del__. + """ + Test that an unraisable exception is not created when initialization + fails. + """ # Pytest would fail the test if such an exception occurred. fig, ax = plt.subplots() with pytest.raises(TypeError, match="unexpected keyword argument 'undefined'"): diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 5e87abe08dc4..43fa505a32be 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -14,6 +14,7 @@ import enum import functools import itertools +import weakref from numbers import Integral, Number from cycler import cycler @@ -120,16 +121,6 @@ def __init__(self, ax): 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 +161,9 @@ def _save_blit_background(self, background): 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() + 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): From d725b1354490a2c030b1f6c7e1fd9a9a2b2c7d5f Mon Sep 17 00:00:00 2001 From: Larry Bradley Date: Fri, 23 Jan 2026 18:19:15 -0500 Subject: [PATCH 5/5] Remove AxesWidget.__del__ from widgets.pyi --- lib/matplotlib/widgets.pyi | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/matplotlib/widgets.pyi b/lib/matplotlib/widgets.pyi index 2f34255d625c..7f4812f7fa60 100644 --- a/lib/matplotlib/widgets.pyi +++ b/lib/matplotlib/widgets.pyi @@ -35,7 +35,6 @@ class Widget: class AxesWidget(Widget): ax: Axes def __init__(self, ax: Axes) -> None: ... - def __del__(self) -> None: ... @property def canvas(self) -> FigureCanvasBase | None: ... def connect_event(self, event: Event, callback: Callable) -> None: ...