Repository: matplotlib/matplotlib

Issue:
[Bug]:  strange behaviour when redefining figure size
### Bug summary

setting figure size using the figsize parameter to matplotlib.pyplot.figure works just fine, but setting it to a different value in the same script fails in case the first argument to the figure call is an identifier.

### Code for reproduction

```Python
import matplotlib.pyplot
# first call
fig = matplotlib.pyplot.figure(1,figsize=(1,2))
fig.get_figheight()
fig.get_figwidth()
# second call
fig = matplotlib.pyplot.figure(1,figsize=(3,4))
fig.get_figheight()
fig.get_figwidth()
```


### Actual outcome

```Python
>>> import matplotlib.pyplot
>>> # first call
>>> fig = matplotlib.pyplot.figure(1,figsize=(1,2))
Qt: Session management error: Could not open network socket
>>> fig.get_figheight()
2.0
>>> fig.get_figwidth()
1.0
>>> # second call
>>> fig = matplotlib.pyplot.figure(1,figsize=(3,4))
>>> fig.get_figheight()
2.0
>>> fig.get_figwidth()
1.0
>>> 
```

### Expected outcome

The second call should change the figure size and the get_figheight/get_figwidth methods should report the changed size.

### Additional information

Note that the problem does not occur if no identifier is provided to the figure call, so this works just fine:
```Python
fig = matplotlib.pyplot.figure(figsize=(3,4))
fig.get_figheight()
fig.get_figwidth()
```
### Operating system

Fedora 39 linux

### Matplotlib Version

3.8.2

### Matplotlib Backend

QtAgg

### Python version

Python 3.12.2

### Jupyter version

n.a.

### Installation

Linux package manager


Discussion:
`figure(1, ...)` returns a figure with the given id if that exists, or creates one if not.

The second call just returns the same figure again. Kim against modifying an existing figure through the interface. We may discuss whether we want to warn on that. I think a recall should not have any parameters. Currently, they are ignored. The only downside would be that you can now do the exact same call twice. But I argue that creation and recall are fundamentally different and the user should know which one they want. (This does not improve the situation for. the no kwargs case, but at least does not yield a figure other than expected.
Well, this behaviour clearly surprised me, and it took me some time to find the issue when working on some code that I did not write myself.
Yes, I think it would be good to issue a warning if a user tries to add additional parameters on a second call.
The syntax of the call suggests that this is an instantiation of a class and therefore would create a new figure with new settings, but clearly this is not the case for the second call.
I think it would be much more clear if there was a second function, called something like get_figure(identifier) to retrieve an already existing figure. Without identifier it could just be the same as gcf()
For context, the current behavior derives from how Matlab behaves. In
Matlab, calling `figure(1)` either creates a new figure with that id, or
gives you back the existing figure. I do think a warning would be
appropriate. Maybe we keep an internal dictionary mapping the figure id to
its instantiation arguments (or perhaps more robustly, store them in the
Figure object itself?).

On Tue, Mar 26, 2024 at 9:54 AM Jos de Kloe ***@***.***>
wrote:

> Well, this behaviour clearly surprised me, and it took me some time to
> find the issue when working on some code that I did not write myself.
> Yes, I think it would be good to issue a warning if a user tries to add
> additional parameters on a second call.
> The syntax of the call suggests that this is an instantiation of a class
> and therefore would create a new figure with new settings, but clearly this
> is not the case for the second call.
> I think it would be much more clear if there was a second function, called
> something like get_figure(identifier) to retrieve an already existing
> figure. Without identifier it could just be the same as gcf()
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/matplotlib/matplotlib/issues/27978#issuecomment-2020492797>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AACHF6C7HIT2A6FP6DWYNW3Y2FVXJAVCNFSM6AAAAABFIZKWRKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDAMRQGQ4TENZZG4>
> .
> You are receiving this because you are subscribed to this thread.Message
> ID: ***@***.***>
>

I agree we should we warning if user input is ignored.  I have a vague memory that we have talked about this before but got stuck on what should we do in the case of

```python
import matplotlib.pyplot
# first call
fig = matplotlib.pyplot.figure(1,figsize=(1,2))
fig.get_figheight()
fig.get_figwidth()
# second call
fig = matplotlib.pyplot.figure(1,figsize=(1,2))
fig.get_figheight()
fig.get_figwidth()
```

On one hand, we should warn because we are reusing the figure and ignoring all of the users input, on the other hand the values match the values that are already on the existing figure so we should not warn because they actually got what the wanted.

It is possible I may be remembering a similar discussion about reusing existing axes?
I can see an argument to warn also for a second identical call with kwargs: while you get the same type of figure, in any non-trivial use case, you‘ll get an empty figure for the first call and a non-empty figure for the second call. These are so fundamentally different that it‘s unlikely you can work with them correctly if you don‘t know from context which one you’ll get. And if you know the figure exists, `figure(N)` just means „make figure N active“ no need to repeat its arguments. Also since this is likely used interactively, the short version without arguments is more convenient.

--

Edit: To give context: What options do we have?

#### Do nothing at all

Disadvantage: The above example: You call with an explicit parameter but get back something different

```
>>> fig = plt.figure(1,figsize=(1,2))
>>> fig = plt.figure(1,figsize=(3,4))
>>> fig.get_figheight()
2.0
>>> fig.get_figwidth()
1.0
```

#### Only warn if the given parameters do not match

Technical note: Detecting this correctly is somewhat cumbersome. You have to store the called parameters; and maybe normalize (e.g. positional vs. kwarg usage).

Usability note: Would we require to replicate all args for the recall, or should we accept the bare `figure(1)` as well even if the first call had the arguments? - IMHO clearly the second. This is primarily for interactive use and forcing to type out all kwargs again would be annoying.

#### Always warn if parameters are given for a recall.

While one could argue that `plt.figure(1)` returning a figure with non-standard parameters is surprising, you really don't know what you are getting from the code because lots of configuration is determined by `rcParams`.

Second, as stated above, recall is mainly for interactive usage, so the short version is desireable, and nobody should need to type out the parameters again. So warning on two identical kwarg calls is ok - it may even be helpful, because you may think you get a new figure, but you don't.

---

Overall for simplicity and safety, I propose to always warn if parameters are given for a recall.
> Overall for simplicity and safety, I propose to always warn if parameters are given for a recall.

I agree with this proposal.



Patch:
diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py
index 3b1a01c28408..52850f128cae 100644
--- a/lib/matplotlib/pyplot.py
+++ b/lib/matplotlib/pyplot.py
@@ -983,30 +983,42 @@ def figure(
     `~matplotlib.rcParams` defines the default values, which can be modified
     in the matplotlibrc file.
     """
+    allnums = get_fignums()
+
     if isinstance(num, FigureBase):
         # type narrowed to `Figure | SubFigure` by combination of input and isinstance
         if num.canvas.manager is None:
             raise ValueError("The passed figure is not managed by pyplot")
+        elif any([figsize, dpi, facecolor, edgecolor, not frameon,
+                  kwargs]) and num.canvas.manager.num in allnums:
+            _api.warn_external(
+                "Ignoring specified arguments in this call "
+                f"because figure with num: {num.canvas.manager.num} already exists")
         _pylab_helpers.Gcf.set_active(num.canvas.manager)
         return num.figure
 
-    allnums = get_fignums()
     next_num = max(allnums) + 1 if allnums else 1
     fig_label = ''
     if num is None:
         num = next_num
-    elif isinstance(num, str):
-        fig_label = num
-        all_labels = get_figlabels()
-        if fig_label not in all_labels:
-            if fig_label == 'all':
-                _api.warn_external("close('all') closes all existing figures.")
-            num = next_num
-        else:
-            inum = all_labels.index(fig_label)
-            num = allnums[inum]
     else:
-        num = int(num)  # crude validation of num argument
+        if any([figsize, dpi, facecolor, edgecolor, not frameon,
+                kwargs]) and num in allnums:
+            _api.warn_external(
+                "Ignoring specified arguments in this call "
+                f"because figure with num: {num} already exists")
+        if isinstance(num, str):
+            fig_label = num
+            all_labels = get_figlabels()
+            if fig_label not in all_labels:
+                if fig_label == 'all':
+                    _api.warn_external("close('all') closes all existing figures.")
+                num = next_num
+            else:
+                inum = all_labels.index(fig_label)
+                num = allnums[inum]
+        else:
+            num = int(num)  # crude validation of num argument
 
     # Type of "num" has narrowed to int, but mypy can't quite see it
     manager = _pylab_helpers.Gcf.get_fig_manager(num)  # type: ignore[arg-type]


Test patch:
diff --git a/lib/matplotlib/tests/test_pyplot.py b/lib/matplotlib/tests/test_pyplot.py
index a077aede8f8b..63dc239df2e8 100644
--- a/lib/matplotlib/tests/test_pyplot.py
+++ b/lib/matplotlib/tests/test_pyplot.py
@@ -457,3 +457,22 @@ def test_figure_hook():
         fig = plt.figure()
 
     assert fig._test_was_here
+
+
+def test_multiple_same_figure_calls():
+    fig = mpl.pyplot.figure(1, figsize=(1, 2))
+    with pytest.warns(UserWarning, match="Ignoring specified arguments in this call"):
+        fig2 = mpl.pyplot.figure(1, figsize=(3, 4))
+    with pytest.warns(UserWarning, match="Ignoring specified arguments in this call"):
+        mpl.pyplot.figure(fig, figsize=(5, 6))
+    assert fig is fig2
+    fig3 = mpl.pyplot.figure(1)  # Checks for false warnings
+    assert fig is fig3
+
+
+def test_close_all_warning():
+    fig1 = plt.figure()
+
+    # Check that the warning is issued when 'all' is passed to plt.figure
+    with pytest.warns(UserWarning, match="closes all existing figures"):
+        fig2 = plt.figure("all")