Repository: ipython/ipython

Issue:
`%reset -f out` does not free memory used by output history
In the following `ipython` session I create and output a large array repeatedly. Memory usage increases with each invocation since all outputs are added to the output history. The `%reset -f out` command clears that output history. However, I notice that it doesn't actually free any memory, even when I follow up with `gc.collect()`.

```
Python 3.11.4 | packaged by conda-forge | (main, Jun 10 2023, 18:08:17) [GCC 12.2.0]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.14.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: !free -m
               total        used        free      shared  buff/cache   available
Mem:           31744        5224       22241        1233        4278       24831
Swap:            975         645         330

In [2]: import numpy as np
   ...: arr = np.arange(250_000_000)
   ...: arr
Out[2]: 
array([        0,         1,         2, ..., 249999997, 249999998,
       249999999])

In [3]: import numpy as np
   ...: arr = np.arange(250_000_000)
   ...: arr
Out[3]: 
array([        0,         1,         2, ..., 249999997, 249999998,
       249999999])

In [4]: import numpy as np
   ...: arr = np.arange(250_000_000)
   ...: arr
Out[4]: 
array([        0,         1,         2, ..., 249999997, 249999998,
       249999999])

In [5]: import numpy as np
   ...: arr = np.arange(250_000_000)
   ...: arr
Out[5]: 
array([        0,         1,         2, ..., 249999997, 249999998,
       249999999])

In [6]: import numpy as np
   ...: arr = np.arange(250_000_000)
   ...: arr
Out[6]: 
array([        0,         1,         2, ..., 249999997, 249999998,
       249999999])

In [7]: Out
Out[7]: 
{2: array([        0,         1,         2, ..., 249999997, 249999998,
        249999999]),
 3: array([        0,         1,         2, ..., 249999997, 249999998,
        249999999]),
 4: array([        0,         1,         2, ..., 249999997, 249999998,
        249999999]),
 5: array([        0,         1,         2, ..., 249999997, 249999998,
        249999999]),
 6: array([        0,         1,         2, ..., 249999997, 249999998,
        249999999])}

In [8]: !free -m
               total        used        free      shared  buff/cache   available
Mem:           31744       14769       12655        1275        4320       15245
Swap:            975         645         330

In [9]: %reset -f out
Flushing output cache (5 entries)

In [10]: Out
Out[10]: {}

In [11]: !free -m
               total        used        free      shared  buff/cache   available
Mem:           31744       14785       12638        1275        4320       15229
Swap:            975         645         330

In [12]: import gc

In [13]: gc.collect()
Out[13]: 714

In [14]: !free -m
               total        used        free      shared  buff/cache   available
Mem:           31744       14800       12623        1275        4320       15214
Swap:            975         645         330
```

It takes a full reset to actually free the memory:

```
In [15]: %reset -f

In [16]: !free -m
               total        used        free      shared  buff/cache   available
Mem:           31744        5253       22169        1275        4321       24761
Swap:            975         645         330
```

I installed IPython the following way:

```
conda create -n ipython-test --override-channels --strict-channel-priority -c conda-forge -c nodefaults ipython numpy
```

Here is the system info:

```
$  python -c "import IPython; print(IPython.sys_info())"
{'commit_hash': '55f5381',
 'commit_source': 'installation',
 'default_encoding': 'utf-8',
 'ipython_path': '/home/admin/anaconda3/envs/ipython-test/lib/python3.11/site-packages/IPython',
 'ipython_version': '8.14.0',
 'os_name': 'posix',
 'platform': 'Linux-6.2.0-26-generic-x86_64-with-glibc2.35',
 'sys_executable': '/home/admin/anaconda3/envs/ipython-test/bin/python',
 'sys_platform': 'linux',
 'sys_version': '3.11.4 | packaged by conda-forge | (main, Jun 10 2023, '
                '18:08:17) [GCC 12.2.0]'}
```


Discussion:
When you do the same in plain Python console (create 5 copies of an array, then delete them, then call `gc.collect`) is the memory released as you are expecting?
Yes, in a plain Python console the memory is released as expected.

Resolving patch:
diff --git a/IPython/core/displayhook.py b/IPython/core/displayhook.py
--- a/IPython/core/displayhook.py
+++ b/IPython/core/displayhook.py
@@ -296,9 +296,14 @@ def flush(self):
 
         for n in range(1,self.prompt_count + 1):
             key = '_'+repr(n)
+            try:
+                del self.shell.user_ns_hidden[key]
+            except KeyError:
+                pass
             try:
                 del self.shell.user_ns[key]
-            except: pass
+            except KeyError:
+                pass
         # In some embedded circumstances, the user_ns doesn't have the
         # '_oh' key set up.
         oh = self.shell.user_ns.get('_oh', None)


Test patch: