Skip to content

[MNT]: Lifetime of pyplot figures #29849

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
timhoffm opened this issue Apr 1, 2025 · 6 comments
Open

[MNT]: Lifetime of pyplot figures #29849

timhoffm opened this issue Apr 1, 2025 · 6 comments

Comments

@timhoffm
Copy link
Member

timhoffm commented Apr 1, 2025

Summary

Inspired by but technically orthogonal to the recent discussions #29782, #29836: Can we change the lifetime behavior so that (1) users have to care/know less about how pyplot manages the figure - ideally no close() is needed - and (2) we do not break typical existing usage patterns?

Proposed fix

Observations / Questions:

  1. Why does pyplot has to keep a figure reference?
    • 1.1: This prevents garbage collection if the user does not store the figure in a variable.
    • 1.2: It allows managing multiple figures to change the current figure.
    • 1.3: It manages the figure interactions if the figure is shown.
  2. It is unlikely that people manage a lot of figures in parallel through the pyplot state - (if you have 20 figures, you'll propably not organize them through pyplot - or if you do, you at least assign dedicated fignums when creating them)

This combination tells me there is a space for criterions to auto-delete figures:

One interpretation could be: a figure is not needed anymore if all of the following apply:

  • the figure is not shown (no interaction possible)
  • the figure is not the active figure (it's currently not worked on)
  • the figure does not have a user-created fignum (it's not possible to make the figure active again)

This would effectively mean that you only have muliple figures tracked in pyplot if either they are currently shown or they have explicit user-created fignums. Conversely, figures without user-created fignums are auto-deleted when appropriate.
IMHO this is a very reasonable logic and removes the need to understand pyplot tracking or use of close for almost all users.

There's only one minor use case I can imagine that would get broken by this: Users relying on the implictly created fignumbers for changing the active figure:

plt.figure()  # num=1
plt.plot()

plt.figure()  # num=2
plt.plot()

plt.figure(1)  # knowing that the first figure() call auto-assigned num=1

But this behavior is brittle anyway because you have to know that you're in a fresh interpreter. There are three mitigation strategies:

  • deprecate recalling a figure with an implicitly created figure number
  • keep the last N (e.g. 5) implicitly created figures still around. This recall by implicit number may be feasible for one or two figures but nobody can do this for many figures
  • don't care as this is a brittle edge-case, which people should not be using anyway
@rcomer
Copy link
Member

rcomer commented Apr 1, 2025

I have some old code that is basically

for fig in [fig1, fig2]:
    ax = fig.add_subplot(3, 3, plot_num)
    plt.sca(ax)
    ... plot stuff ...

I likely would not write it using plt.sca now (the blame says it's at least 7 year old!) but there are third party packages that insist on using the current axes, so I think it is not an unreasonable pattern.

It might be nice if pyplot could register an already-existing figure - I assume there are technical reasons not to let it?

@timhoffm
Copy link
Member Author

timhoffm commented Apr 1, 2025

Good point, that way (or generally by passing a Figure instance plt.figure(fig)) one can make a figure current again. This would if the figure was removed from pyplot according to the above proposal.

While it's currently prohibited, I don't think there would be a fundamental problem whith registering an already-existing figure. Actually making is possible to add/remove figures from pyplot tracking would make the pyplot code less entangled.


Edit: Actually, we already can attach an existing figure to pyplot and use that for unpickling Figure:

if restore_to_pylab:
# lazy import to avoid circularity
import matplotlib.pyplot as plt
import matplotlib._pylab_helpers as pylab_helpers
allnums = plt.get_fignums()
num = max(allnums) + 1 if allnums else 1
backend = plt._get_backend_mod()
mgr = backend.new_figure_manager_given_figure(num, self)
pylab_helpers.Gcf._set_new_active_manager(mgr)
plt.draw_if_interactive()

@rcomer
Copy link
Member

rcomer commented Apr 1, 2025

Slightly tangential, but I also just quite like the idea that you could decide after the fact that you want your figure in a gui.

my_fig = matplotlib.figure.Figure(...)
ax = my_fig.subplots()
... add artists ...

plt.figure(my_fig)
plt.show()

@timhoffm
Copy link
Member Author

timhoffm commented Apr 1, 2025

Slightly tangential, but I also just quite like the idea that you could decide after the fact that you want your figure in a gui.

I have a prototype working for this 😄

timhoffm added a commit to timhoffm/matplotlib that referenced this issue Apr 1, 2025
It may be fundamentally nice not to have to create the figure
though pyplot to be able to use it in pyplot afterwards. You can now do

```
from matplotlib.figure import Figure
import matplotlib.pyplot as plt

fig = Figure()
fig.subplots().plot([1, 3, 2])

plt.figure(fig)  # fig is now tracked in pyplot
plt.show()
```

This also opens up the possibility to more dynamically track
and untrack figures in pyplot, which opens up the road to
optimized figure tracking in pyplot (matplotlib#29849)
timhoffm added a commit to timhoffm/matplotlib that referenced this issue Apr 1, 2025
It may be fundamentally nice not to have to create the figure
though pyplot to be able to use it in pyplot afterwards. You can now do

```
from matplotlib.figure import Figure
import matplotlib.pyplot as plt

fig = Figure()
fig.subplots().plot([1, 3, 2])

plt.figure(fig)  # fig is now tracked in pyplot
plt.show()
```

This also opens up the possibility to more dynamically track
and untrack figures in pyplot, which opens up the road to
optimized figure tracking in pyplot (matplotlib#29849)
timhoffm added a commit to timhoffm/matplotlib that referenced this issue Apr 1, 2025
It may be fundamentally nice not to have to create the figure
though pyplot to be able to use it in pyplot afterwards. You can now do

```
from matplotlib.figure import Figure
import matplotlib.pyplot as plt

fig = Figure()
fig.subplots().plot([1, 3, 2])

plt.figure(fig)  # fig is now tracked in pyplot
plt.show()
```

This also opens up the possibility to more dynamically track
and untrack figures in pyplot, which opens up the road to
optimized figure tracking in pyplot (matplotlib#29849)
@rcomer
Copy link
Member

rcomer commented Apr 2, 2025

I think this will also break the case where you do not care how your figures are tracked, but want to show them all together. This is probably quite common.

for ds, name in zip(my_datasets, dataset_names):
    fig, ax = plt.subplots()
    ax.plot(ds, ...)
    ax.set_title(name)

plt.show()

@timhoffm
Copy link
Member Author

timhoffm commented Apr 2, 2025

This example breaks my assumption that you need to somehow be able to retrieve an individual figure again.

The two variants with/wo the last show are possible and common:

for ds, name in zip(my_datasets, dataset_names):
    fig, ax = plt.subplots()
    ax.plot(ds, ...)
    ax.set_title(name)
    plt.savefig(f"{name}.png")

plt.show()  # Comment or uncomment

We can at no time know whether the figure is still needed because there could always be a plt.show() down the road. This means we cannot detect the "just savefig" case, which is the common cause for memory buildup. 😢

We cannot just be clever and auto-clean while keeping full backward-compatibility for relevant use cases. One could still think about the reasonable lifetime for pyplot figure management, but there would be breaking changes at least for some usage scenarios, which makes it much less attractive.

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

No branches or pull requests

2 participants