API: Raise ValueError in subplots if num refers to existing figure#31230
Conversation
d9f11c2 to
06ab8ab
Compare
lib/matplotlib/pyplot.py
Outdated
|
|
||
| """ | ||
| num = fig_kw.get('num') | ||
| if num is not None and fignum_exists(num): |
There was a problem hiding this comment.
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]: FalseThere was a problem hiding this comment.
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!
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
|
I think this can be independent. |
PR valid for other reasons than the linked issue
|
Should we have the same restriction for |
@rcomer That's an excellent suggestion. It makes sense to keep the behavior consistent across both utility functions. |
06ab8ab to
2aa3085
Compare
|
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. |
|
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 |
e922ee3 to
c9ba0e6
Compare
|
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: to layout a figure. |
c9ba0e6 to
f8f0a92
Compare
"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!" |
f8f0a92 to
89a60fe
Compare
|
@Aaratrika-Shelly many of your responses here read like they are AI generated. Please read our policy on that. |
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. |
lib/matplotlib/pyplot.py
Outdated
| num = fig_kw.get('num') | ||
| if num is not None: | ||
|
|
||
| if isinstance(num, Figure): |
lib/matplotlib/pyplot.py
Outdated
| 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().") |
There was a problem hiding this comment.
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.
|
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 |
89a60fe to
ab5e1c7
Compare
|
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 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. |
|
The point is that plt.subplots documents itself as
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 |
| 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. |
There was a problem hiding this comment.
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.
lib/matplotlib/tests/test_pyplot.py
Outdated
| 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I have removed the test case. I got confused about what was needed.
ab5e1c7 to
904d093
Compare
|
Thank you for your work on this @Aaratrika-Shelly. I think this is ready to go and anyone can merge when CI has passed. |
|
Thanks @Aaratrika-Shelly ! |
…y/fix/subplots-reuse-error
…y/fix/subplots-reuse-error
PR summary
This PR updates
pyplot.subplotsto raise aValueErrorif thenumparameter refers to an existing figure, unlessclear=Trueis also provided.Why is this change necessary?
plt.subplots(num=1)
) is ambiguous and can lead to inconsistent state or errors (likeValueError: 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
subplotscalls, 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 withclear=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