On macOS 26 (Tahoe), using the native MacOSX backend in an interactive shell causes the window to persist in the system's window server after it has been closed. While the window content disappears, a blank "ghost" space remains visible in Mission Control and is still detected/managed by tiling window managers (like Yabai).
The window only fully disappears once the Python process (the shell) is terminated.
I have tested with older versions of matplotlib and Python and this began to happen on macOS 26. This does not happen on my linux machine.
If you open 2 windows and close one do you get a ghost of the closed one?
If you close one and then open a second one, does the first one hang around / do they accumulate?
Hopefully these questions will help narrow down where the bug is (really leaking windows vs just not running the event loop long enough for the windows to finish cleaning up (even if we don't have any windows we still know about).
It spawns the two figures in one window each in their own tab. Closing one tab closes the figure (no ghost) but closing the final figure creates the ghost. If I drag the tab out to create two windows (one per figure) then I do get a ghost each time I close each window. Maybe this is a clue?
If I install pyqt5 and use the qt5agg backend, I do not get this issue.
I thought I was going crazy, because it seemed that randomly I couldn't interact with figure windows. But now I know it is the ghost windows that overlay the other figures, so you can't interact with it with the mouse, although it does have focus:
say I programmatically create 6 figures in a script; they all open in the same location, so I have a stack of 6 figure windows.
I close the first one (Figure 6) with the red bubble in the top-left of the window; focus goes to Figure 5
I now see Figure 5, but I can't interact with Figure 5, because Figure 6's ghost window is on top of it. As a result, I cannot close Figure 5 by clicking the red bubble
I can drag the ghost window out of the way by trying to move Figure 5, which doesn't move.
Now I can close Figure 5 with the red bubble and a see Figure 4
Again I can't interact with Figure 4, because Figure 5's ghost window is on top.
...etc...
I can close all the Figures with Cmd + W: after 6 times, all Figures are closed, so the focus of the figure windows correctly goes to the next Figure window, it doesn't not remain on the ghost window.
Not sure if this is helpful, but maybe it can shed some light on what's happening.
I'm not able to reproduce/test this, but if anyone is able to install from source and wants to try debugging it, it seems like it might be something to do with keeping a reference around after closing the window, maybe something going wrong in our destroy to not DECREF the object enough or something else is holding a reference still... We've had other memory leaks reported that it may be related to as well #21788
Likely location to look for something going awry when trying to close the window:
Hello, 25+ year Mac developer here, although I am fairly terrible at Python :)
The root cause with a lot of these issues is that you need to call -release between lines 699 and 700 in order to balance the calls to +alloc/-init. You also need to clear the delegate to prevent a potential crash.
@greglucas - Let me try to wrap my head around how Python interacts with native code and what the object ownership graph should look like. At first glance, it looks like there are some memory leaks and potential crashers as well, at least from the Obj-C side of things.
Honestly, I've long had "make matplotlib behave as a native macOS app" on my list of potential projects, as several built-in macOS window management features and keyboard shortcuts have never worked, but I've never gotten around to investigating deeper.
I've forked matplotlib and I am working on debugging this.
My previous code example is partially incorrect. The window in question has isReleasedWhenClosed set to YES, so -close should release it per API documentation. However, isReleasedWhenClosed is typically set to NO since the introduction of ARC1. It is also automatically set to NO when is owned by an NSWindowController.
Neither should be the case here and the code should work. That said, isReleasedWhenClosed=YES combined with manual reference counting probably lacks test coverage inside of Apple, so Tahoe could have broken this.
My main development machine is running Sonoma. I cannot reproduce the ghost window issue, but I'm seeing windows fail to be -dealloc'd after closing.
I fired up a clean install of Tahoe on a test machine, but I still cannot reproduce the ghost window issue. I am seeing the windows fail to be properly deallocated after closing (due to #31751).
My best guess is that the way we were closing windows (and not releasing them) exposed a bug in WindowServer/SkyLight that Apple fixed in 26.3 or later.
I think that #31769 would have fixed this, but I can't be 100% certain. If anybody reading this is still on 26.2 and can test after #31769 lands, I'd be interested in knowing the results!
Activity
tacaswell commented on Feb 6, 2026
If you open 2 windows and close one do you get a ghost of the closed one?
If you close one and then open a second one, does the first one hang around / do they accumulate?
Hopefully these questions will help narrow down where the bug is (really leaking windows vs just not running the event loop long enough for the windows to finish cleaning up (even if we don't have any windows we still know about).
tacaswell commented on Feb 6, 2026
I wonder if #31066 is related?
TheSkyentist commented on Feb 6, 2026
In a single interactive python session, I can do the following:
I can repeat this to get arbitrary numbers of ghosts.
If I run the following code:
It spawns the two figures in one window each in their own tab. Closing one tab closes the figure (no ghost) but closing the final figure creates the ghost. If I drag the tab out to create two windows (one per figure) then I do get a ghost each time I close each window. Maybe this is a clue?
If I install pyqt5 and use the qt5agg backend, I do not get this issue.
lcvisser commented on Feb 27, 2026
I thought I was going crazy, because it seemed that randomly I couldn't interact with figure windows. But now I know it is the ghost windows that overlay the other figures, so you can't interact with it with the mouse, although it does have focus:
I can close all the Figures with Cmd + W: after 6 times, all Figures are closed, so the focus of the figure windows correctly goes to the next Figure window, it doesn't not remain on the ghost window.
Not sure if this is helpful, but maybe it can shed some light on what's happening.
greglucas commented on Feb 28, 2026
I'm not able to reproduce/test this, but if anyone is able to install from source and wants to try debugging it, it seems like it might be something to do with keeping a reference around after closing the window, maybe something going wrong in our destroy to not DECREF the object enough or something else is holding a reference still... We've had other memory leaks reported that it may be related to as well #21788
Likely location to look for something going awry when trying to close the window:
matplotlib/src/_macosx.m
Lines 696 to 721 in 67ebfc9
iccir commented on May 25, 2026
Hello, 25+ year Mac developer here, although I am fairly terrible at Python :)
The root cause with a lot of these issues is that you need to call -release between lines 699 and 700 in order to balance the calls to +alloc/-init. You also need to clear the delegate to prevent a potential crash.
The lines should look like this:
static PyObject* FigureManager_destroy(FigureManager* self) { [self->window close]; [self->window setDelegate:nil]; [self->window release]; self->window = NULL;greglucas commented on May 25, 2026
@iccir interested in making a PR with those updates if they work for you? Contributions are more than welcome!
iccir commented on May 25, 2026
@greglucas - Let me try to wrap my head around how Python interacts with native code and what the object ownership graph should look like. At first glance, it looks like there are some memory leaks and potential crashers as well, at least from the Obj-C side of things.
Honestly, I've long had "make matplotlib behave as a native macOS app" on my list of potential projects, as several built-in macOS window management features and keyboard shortcuts have never worked, but I've never gotten around to investigating deeper.
iccir commented on May 26, 2026
I've forked matplotlib and I am working on debugging this.
My previous code example is partially incorrect. The window in question has isReleasedWhenClosed set to YES, so -close should release it per API documentation. However, isReleasedWhenClosed is typically set to NO since the introduction of ARC1. It is also automatically set to NO when is owned by an NSWindowController.
Neither should be the case here and the code should work. That said, isReleasedWhenClosed=YES combined with manual reference counting probably lacks test coverage inside of Apple, so Tahoe could have broken this.
My main development machine is running Sonoma. I cannot reproduce the ghost window issue, but I'm seeing windows fail to be -dealloc'd after closing.
Footnotes
releasedWhenClosed ↩
Add @autoreleasepool blocks (matplotlib#31106)
iccir commented on May 28, 2026
@TheSkyentist / @lcvisser - Do either of you have the ability to easily build/test my fork?
I fired up a clean install of Tahoe on a test machine, but I still cannot reproduce the ghost window issue. I am seeing the windows fail to be properly deallocated after closing (due to #31751).
lcvisser commented on Jun 3, 2026
@iccir I can no longer reproduce the issue either. I've been upgraded to 26.3 in the mean time, perhaps that fixed something?
TheSkyentist commented on Jun 3, 2026
I can also no longer reproduce this bug either... could also be due to macOS updates in the meantime, I am now on 26.5
iccir commented on Jun 3, 2026
My best guess is that the way we were closing windows (and not releasing them) exposed a bug in WindowServer/SkyLight that Apple fixed in 26.3 or later.
I think that #31769 would have fixed this, but I can't be 100% certain. If anybody reading this is still on 26.2 and can test after #31769 lands, I'd be interested in knowing the results!