Skip to content

API: Raise ValueError in subplots if num refers to existing figure#31230

Merged
timhoffm merged 1 commit intomatplotlib:mainfrom
Aaratrika-Shelly:fix/subplots-reuse-error
Mar 11, 2026
Merged

API: Raise ValueError in subplots if num refers to existing figure#31230
timhoffm merged 1 commit intomatplotlib:mainfrom
Aaratrika-Shelly:fix/subplots-reuse-error

Conversation

@Aaratrika-Shelly
Copy link
Copy Markdown
Contributor

@Aaratrika-Shelly Aaratrika-Shelly commented Mar 3, 2026

PR summary

This PR updates pyplot.subplots to raise a ValueError if the num parameter refers to an existing figure, unless clear=True is also provided.

  • Why is this change necessary?
    plt.subplots(num=1)) is ambiguous and can lead to inconsistent state or errors (like ValueError: The passed figure is not managed by pyplot`) in interactive environments, as described in the issue.

  • What problem does it solve?
    It prevents users from unintentionally reusing figures in subplots calls, which is not the intended usage pattern.

  • What is the reasoning for this implementation?
    Following the discussion with @timhoffm, the cleanest fix is to prohibit this pattern directly. A helpful error message guides the user to either retrieve the figure with plt.figure(num) or clear it with clear=True.

AI Disclosure

I used AI tools to help proofread and improve the grammar of this PR description. The code logic and implementation are my own work.

PR checklist


"""
num = fig_kw.get('num')
if num is not None and fignum_exists(num):
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.

The case from #31223 passed num=fig, where fig is a Figure instance. fignum_exists only works on integers, so I do not think this helps #31223.

In [1]: import matplotlib

In [2]: matplotlib.__version__
Out[2]: '3.10.8'

In [3]: import matplotlib.pyplot as plt

In [4]: fig = plt.figure()

In [5]: plt.fignum_exists(fig)
Out[5]: False

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you for catching that, @rcomer. You're absolutely right—fignum_exists only handles the integer/string case, so my current logic wouldn't have caught the num=fig pattern from the original issue.
I implemented it this way based on the earlier suggestion, but I see now we need a more robust check. Is the preferred approach here to add an explicit isinstance(num, Figure) check to see if num.canvas.manager exists, or is there a more unified helper in the codebase I should use to check if a "number or object" is already managed by pyplot?
I'm happy to update the logic and add a test case for passing the figure object once the best approach is decided. Thanks!

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.

I'd go for the narrow check: Only allow int or str that are not an existing fignum. We've historically been a bit vague what fignum can be but we should narrow that down.

rcomer
rcomer previously requested changes Mar 3, 2026
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.

I think we need to put this on hold until we confirm whether passing the figure to subplots really was the cause of the error in #31223. My check suggests not, but perhaps I made a mistake https://github.com/matplotlib/matplotlib/issues/31223#issuecomment-3992077696.

@timhoffm
Copy link
Copy Markdown
Member

timhoffm commented Mar 3, 2026

I think this can be independent. plt.subplots() should never act on existing figures, because that's a recipe for logical chaos, even if it does not cause matplotlib/ipympl#622.

@rcomer
Copy link
Copy Markdown
Member

rcomer commented Mar 3, 2026

Should we have the same restriction for pyplot.subplot_mosaic?

@Aaratrika-Shelly
Copy link
Copy Markdown
Contributor Author

Should we have the same restriction for pyplot.subplot_mosaic?

@rcomer That's an excellent suggestion. It makes sense to keep the behavior consistent across both utility functions.
I will go ahead and apply the same logic to subplot_mosaic, and I'll update the subplots check to explicitly block Figure instances as @timhoffm suggested. I'll push an updated commit once I've added tests for both scenarios. Thanks!

@Aaratrika-Shelly
Copy link
Copy Markdown
Contributor Author

It looks like the new tests in test_pyplot.py are passing, but there are two failures in test_colorbar.py. Based on the traceback, these seem to be AssertionErrors unrelated to the ValueError logic I added to subplots. Please let me know if you'd like me to investigate further or if these are known flaky tests in the CI.

@rcomer
Copy link
Copy Markdown
Member

rcomer commented Mar 3, 2026

I have not seen those colorbar failures before, but I do not see how they could have been caused by this PR. Since they are in the minimum version tests and involve logarithms and ticks, I suspect they have the same cause as the ones addressed at #30950.

The changenote needs updating to also mention subplot_mosaic.

@Aaratrika-Shelly Aaratrika-Shelly force-pushed the fix/subplots-reuse-error branch 2 times, most recently from e922ee3 to c9ba0e6 Compare March 4, 2026 19:03
@tacaswell
Copy link
Copy Markdown
Member

I had some question if we want to go straight to error or start with a warning, but Tim's point in the issue about now wanting to accidentally register a figure with pyplot is a good one so we should go straight to error even if we risk breaking some users. I am sure there exists at least one user who is doing:

plt.subplots(3, 3, num=1, gridspec_kw={'left': 0, 'right':.5})
plt.subplots(num=1, gridspec_kw={'left': .5, 'right':1})

to layout a figure.

@Aaratrika-Shelly Aaratrika-Shelly force-pushed the fix/subplots-reuse-error branch from c9ba0e6 to f8f0a92 Compare March 5, 2026 20:28
@Aaratrika-Shelly
Copy link
Copy Markdown
Contributor Author

I had some question if we want to go straight to error or start with a warning, but Tim's point in the issue about now wanting to accidentally register a figure with pyplot is a good one so we should go straight to error even if we risk breaking some users. I am sure there exists at least one user who is doing:

plt.subplots(3, 3, num=1, gridspec_kw={'left': 0, 'right':.5})
plt.subplots(num=1, gridspec_kw={'left': .5, 'right':1})

to layout a figure.

"That's a great example, @tacaswell. I've added a specific test case (test_subplots_error_on_double_call) to ensure that this pattern is correctly caught and raises the intended ValueError. All tests are passing!"

@Aaratrika-Shelly Aaratrika-Shelly force-pushed the fix/subplots-reuse-error branch from f8f0a92 to 89a60fe Compare March 5, 2026 20:32
@rcomer rcomer removed their request for review March 6, 2026 08:46
@rcomer
Copy link
Copy Markdown
Member

rcomer commented Mar 6, 2026

@Aaratrika-Shelly many of your responses here read like they are AI generated. Please read our policy on that.
https://matplotlib.org/devdocs/devel/contribute.html#generative-ai

@Aaratrika-Shelly
Copy link
Copy Markdown
Contributor Author

@Aaratrika-Shelly many of your responses here read like they are AI generated. Please read our policy on that.
https://matplotlib.org/devdocs/devel/contribute.html#generative-ai

I am so sorry. Since I am pretty new to contributing in open source, I was using AI to help proofread my messages (as mentioned in the AI disclosure) to make sure I was being professional. I will not do that from now on.
The code has been written and understood by me.

num = fig_kw.get('num')
if num is not None:

if isinstance(num, Figure):
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.

Please check for FigureBase.

Comment on lines +1860 to +1872
if num is not None:

if isinstance(num, Figure):
raise ValueError(
f"num {num!r} cannot be a Figure instance. plt.subplots() "
"is for creating new figures. To add subplots to an existing "
"figure, use fig.subplots() instead.")

if fignum_exists(num) and not fig_kw.get('clear'):
raise ValueError(
f"Figure {num!r} already exists. Use plt.figure({num!r}) "
"to get it or plt.close({num!r}) to close it. "
"Alternatively, pass 'clear=True' to subplots().")
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.

Please factor this out in a helper raise_if_figure_exists(num, func_name)

so that we can do raise_if_figure_exists(num, "subplots()") here.

The main reason is so that the validation details do not clutter the main task of the function. A secondary effect is to remove the redundancy / keep the messages in sync on both functions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

@QuLogic
Copy link
Copy Markdown
Member

QuLogic commented Mar 7, 2026

I've edited this to remove the closing magic tag as this PR does not fix the original issue directly, and the issue's been moved to the ipympl repo.

@Aaratrika-Shelly Aaratrika-Shelly force-pushed the fix/subplots-reuse-error branch from 89a60fe to ab5e1c7 Compare March 7, 2026 19:03
@rcomer
Copy link
Copy Markdown
Member

rcomer commented Mar 9, 2026

I think I am still confused about the rationale for doing this, and the explanation in the changenote is not really clearing that confusion. What do we mean by "inconsistent state"? What do we mean by "desynchronized internal state"?

We are happy with the idea that you can use plt.figure to either switch to an existing figure managed by pyplot or register one that previously wasn't with pyplot. All plt.subplots does is follow that with a call to fig.subplots(). What is the added danger there?

On the point about not wanting to accidentally register a figure, I agree that this would be a weird pattern

fig = Figure()
plt.subplots(..., num=fig)

but it is explicit so I think hard to do accidentally.

@timhoffm
Copy link
Copy Markdown
Member

timhoffm commented Mar 9, 2026

The point is that plt.subplots documents itself as

Create a figure and a set of subplots.

It is not intended to be used with existing figures, and I consider this a feature because it is a simple "create the basic structure" function. Supporting num needed so that you can give the figure a name. That is legitimate. Since this is internally realized by plt.figure(num) we accidentally accept existing figures (pyplot nums or now even standalone Figures). We only want "create a figure with this num" but since plt.figure(num) has a richer semantics that's inherited.

Comment on lines +8 to +10
This prevents creating subplots or mosaics on an existing figure without
explicitly clearing it, which can lead to inconsistent state. Previously,
passing a ``Figure`` instance could lead to a desynchronized internal state.
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.

I do not understand this paragraph. Please can you update it to either make it clearer what problematic state the figure could get into, or to reflect the explanation given at #31230 (comment), which I think is clear.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

Comment on lines +576 to +588
def test_subplots_error_on_double_call():
"""
Test that calling subplots twice on the same figure raises an error.
This ensures that multiple calls to subplots on the same figure number
without clear=True are correctly prohibited.
"""

plt.subplots(3, 3, num=1, gridspec_kw={'left': 0, 'right': 0.5})

with pytest.raises(ValueError, match="already exists"):
plt.subplots(num=1, gridspec_kw={'left': 0.5, 'right': 1})

plt.close(1)
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.

Do we really need this test? I'm struggling to think of a scenario that might break this one without also breaking test_subplots_reuse_existing_figure_error.

My reading of @tacaswell's comment was not that we explicitly want to prohibit this sort of case, but rather this is a fairly reasonable case that will be prohibited as a side-effect of this change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have removed the test case. I got confused about what was needed.

@Aaratrika-Shelly Aaratrika-Shelly force-pushed the fix/subplots-reuse-error branch from ab5e1c7 to 904d093 Compare March 11, 2026 20:26
@rcomer rcomer added this to the v3.11.0 milestone Mar 11, 2026
@rcomer
Copy link
Copy Markdown
Member

rcomer commented Mar 11, 2026

Thank you for your work on this @Aaratrika-Shelly. I think this is ready to go and anyone can merge when CI has passed.

@timhoffm timhoffm merged commit ffb2e6e into matplotlib:main Mar 11, 2026
34 of 37 checks passed
@timhoffm
Copy link
Copy Markdown
Member

Thanks @Aaratrika-Shelly !

andreas16700 added a commit to andreas16700/matplotlib that referenced this pull request Mar 16, 2026
andreas16700 added a commit to andreas16700/matplotlib that referenced this pull request Mar 16, 2026
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.

5 participants