Skip to content

FIX: Guard against already-removed labels in ContourSet.remove()#31431

Open
buddy0452004 wants to merge 9 commits intomatplotlib:mainfrom
buddy0452004:fix-contour-remove-crash-v2
Open

FIX: Guard against already-removed labels in ContourSet.remove()#31431
buddy0452004 wants to merge 9 commits intomatplotlib:mainfrom
buddy0452004:fix-contour-remove-crash-v2

Conversation

@buddy0452004
Copy link
Copy Markdown

@buddy0452004 buddy0452004 commented Apr 1, 2026

When contour labels are manually removed before calling CS.remove(), matplotlib crashes with a ValueError.

Based on discussion in #31404, this PR:

  1. Adds a UserWarning in ContourSet.remove() when labels were already manually removed, guiding users to remove the entire ContourSet and recreate it instead.
  2. Adds a note in clabel()'s Returns documentation that the returned Text instances should not be individually removed or have their geometry modified.
  3. Adds a regression test that expects the UserWarning.

Closes #31404

AI Disclosure

No AI used.

PR Checklist

Copy link
Copy Markdown
Member

@rcomer rcomer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @buddy0452004, but I do not think you understood the root cause of why the failure happens. I think the explanation at #31404 (comment) is correct. The error shown in the issue states that we tried to remove something from a list that is not in the list. So we got hold of the list axes.texts without problem, but the labels were no longer in there.


def test_contour_remove_after_label_removed():
# Test that CS.remove() works even if labels were manually removed first
# Regression test for https://github.com/matplotlib/matplotlib/issues/31404
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not typically reference issues in test comments.

x = np.linspace(-3.0, 3.0, 20)
y = np.linspace(-2.0, 2.0, 20)
X, Y = np.meshgrid(x, y)
Z = np.exp(-X**2 - Y**2)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For efficiency, try to use the simplest data input you can that still makes the test effective. Look at test_contourf_rasterize for an example.

Copy link
Copy Markdown
Contributor

@Chirag3841 Chirag3841 Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rcomer Yes, that makes sense , I had also suggested using simpler input data in the discussion #31404 .




def test_contour_remove_after_label_removed():
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there is already a test_contour_remove further up the file, I would put this one right below it, to keep tests with similar purposes together.

for text in self.labelTexts:
text.remove()
for text in list(self.labelTexts):
if axes is not None and text in axes.texts:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we check whether axes is None here?

@buddy0452004
Copy link
Copy Markdown
Author

@rcomer Thanks for the review! You're right about the root cause the issue is that the labels were already removed from axes.texts, not that axes is None. Updated the fix to use try/except ValueError instead, which is simpler and correctly addresses the actual failure.

Also:

  • Removed the issue reference from the test comment
  • Simplified test data to use np.arange(16).reshape((4, 4))
  • Moved the test right below test_contour_remove

@timhoffm
Copy link
Copy Markdown
Member

timhoffm commented Apr 1, 2026

I'm putting this on hold until we have clarified what is actually needed. #31404 (comment)

@timhoffm timhoffm marked this pull request as draft April 1, 2026 12:31
@buddy0452004
Copy link
Copy Markdown
Author

@timhoffm @rcomer Updated based on the discussion on #31404:

  1. Added a UserWarning in the except block explaining that some labels were manually removed and guiding users to remove the entire ContourSet and recreate it instead.

  2. Added a note in clabel()'s Returns documentation that the returned Text instances should not be individually removed or have their geometry modified.

@Chirag3841
Copy link
Copy Markdown
Contributor

Chirag3841 commented Apr 3, 2026

@timhoffm @rcomer Updated based on the discussion on #31404:

  1. Added a UserWarning in the except block explaining that some labels were manually removed and guiding users to remove the entire ContourSet and recreate it instead.
  2. Added a note in clabel()'s Returns documentation that the returned Text instances should not be individually removed or have their geometry modified.

According to me, the minimal approach related to this issue by adding the note in clabel() is enough as these changes affecting the colorbar ticks .

@rcomer
Copy link
Copy Markdown
Member

rcomer commented Apr 3, 2026

these changes affecting the colorbar ticks .

Sorry, I'm not following. What is happening to the colorbar ticks?

@Chirag3841
Copy link
Copy Markdown
Contributor

Chirag3841 commented Apr 3, 2026

these changes affecting the colorbar ticks .

Sorry, I'm not following. What is happening to the colorbar ticks?

[gw1] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python

fmt = '%4.2e'

    @pytest.mark.parametrize('fmt', ['%4.2e', '{x:.2e}'])
    def test_colorbar_format(fmt):
        # make sure that format is passed properly
        x, y = np.ogrid[-4:4:31j, -4:4:31j]
        z = 120000*np.exp(-x**2 - y**2)
    
        fig, ax = plt.subplots()
        im = ax.imshow(z)
        cbar = fig.colorbar(im, format=fmt)
        fig.canvas.draw()
        assert cbar.ax.yaxis.get_ticklabels()[4].get_text() == '8.00e+04'
    
        # make sure that if we change the clim of the mappable that the
        # formatting is *not* lost:
        im.set_clim([4, 200])
        fig.canvas.draw()
        assert cbar.ax.yaxis.get_ticklabels()[4].get_text() == '2.00e+02'
    
        # but if we change the norm:
        im.set_norm(LogNorm(vmin=0.1, vmax=10))
        fig.canvas.draw()
>       assert (cbar.ax.yaxis.get_ticklabels()[0].get_text() ==
                '$\\mathdefault{10^{-2}}$')
E       AssertionError: assert '$\\mathdefault{10^{-1}}$' == '$\\mathdefault{10^{-2}}$'
E         - $\mathdefault{10^{-2}}$
E         ?                    ^
E         + $\mathdefault{10^{-1}}$
E         ?                    ^

lib/matplotlib/tests/test_colorbar.py:636: AssertionError

But I am not 100% sure that, this error is related to code or not .

@rcomer
Copy link
Copy Markdown
Member

rcomer commented Apr 3, 2026

That one is a sporadic flaky test, so not related to this PR.

@buddy0452004
Copy link
Copy Markdown
Author

I had removed all labels in a loop thinking it would better trigger the warning, and added a second cs.clabel() call thinking it would make the scenario more complete but neither was relevant to what the test is actually checking. Simplified to just removing one label as suggested.
@timhoffm all your review feedback has been addressed removed the list() wrapping, switched to _api.warn_external() fixed the docstring wording and backticks, and simplified the test. Thanks for the detailed review.

@timhoffm timhoffm marked this pull request as ready for review April 3, 2026 13:50
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.

[Bug]: Crash when removing contour set after removing contour labels

4 participants