Skip to content

[Bug]: Crash when removing a subfigure with a subplot in a figure #31319

Closed

Description

@williamlus

Bug summary

The program crashes with "AttributeError: 'list' object has no attribute '_mouseover_set'" when I create a figure with subfigures, create subplots in the subfigures, and remove a subfigure.

Code for reproduction

import matplotlib.pyplot as plt

fig = plt.figure()
subfigs = fig.subfigures(1, 2)
subfigs[0].subplots()
subfigs[0].remove()

Actual outcome


AttributeError Traceback (most recent call last)
Cell In[2], line 8
6 subfigs[0].subplots()
7 subfigs[1].subplots()
----> 8 subfigs[0].remove()

File ~/dataviz-render-update-testing/matplotlib/lib/matplotlib/artist.py:260, in Artist.remove(self)
257 _ax_flag = False
258 if hasattr(self, 'axes') and self.axes:
259 # remove from the mouse hit list
--> 260 self.axes._mouseover_set.discard(self)
261 self.axes.stale = True
262 self.axes = None # decouple the artist from the Axes

AttributeError: 'list' object has no attribute '_mouseover_set'

Expected outcome

No crash.

Additional information

No response

Operating system

No response

Matplotlib Version

0.2.0.dev53882+unknown.g607d2c380.g02b42db27

Matplotlib Backend

No response

Python version

No response

Jupyter version

No response

Installation

None

Activity

  1. tacaswell commented on Mar 17, 2026

    @tacaswell
    Member

    The problem is that we everything inherits from Artist (including the Figure family of clases), but the type of obj.axes is not stable across all classes: for the Figure family it is list[Axes], for Axes it is None, and for everything else it is None | Axes [Yes this is non-ideal, no we would not do it this way if designing from scratch now, no it is not worth the deprecation cost to change it at this point]. Additionally we customize the remove functionality by setting a callback at run time. Thus, the assumption in the base Artist.remove is that if the artists have a remove callback set and the self.axes attribute is not falsey, then we are in the third category so we can treat the self.axes as an Axes object and need to clean up Axes stuff that was set in add_artist.

    Looking at this I see a couple of paths:

    • update the _remove_method callback for sub-figures to recursively remove their axes/subfigures so that by the time we hit the code is Artist.remove self.axes == []
    • update Artist.remove to bail on self.axes being a list
    • update Artist.remove to bail on Figure subclasses of self
    • update Artist.romove to only not bail on Axes of self.axes
    • add a custom remove method someplace in the Figure family

    None of these are super great, but I think overriding the remove method is the worst choice (mixing methods of injecting logic always makes things worse). Making sure SubFigures recursively clean them selves will make removing them a bit more expensive and will complicate some future plans but might help with releasing memory (breaks reference cycles) and is more consistent with the current behavior else where (e.g. this code block where we are making sure that for a data artist (Line2D) that is being removed from an Axes that they get fully decoupled). Changing the test from the truthyness of self.axes to a type check + not None check is probably the minimal change that will fix the problem with the fewest side effects.

  2. added this to the v3.10.9 milestone on Mar 17, 2026
  3. QuLogic commented on Mar 17, 2026

    @QuLogic
    Member

    Oh, that's unfortunate. We actually have a test for SubFigure removal, but it's an empty one:

    def test_subfigure_remove():
    fig = plt.figure()
    sfs = fig.subfigures(2, 2)
    sfs[1, 1].remove()
    assert len(fig.subfigs) == 3

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions