-
-
Notifications
You must be signed in to change notification settings - Fork 30.3k
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
traceback.clear_frames
does not clear locals when there have been previous access to f_locals
#113939
Comments
f_locals might still contain references to the local vars. Fix python#113939.
Note, another workaround is to do:
in |
I just realize, there is maybe another related bug: When |
Note, my workaround is now to use this function: def traceback_clear_frames(tb):
"""
Clear traceback frame locals.
Just like :func:`traceback.clear_frames`, but has an additional fix
(https://github.com/python/cpython/issues/113939).
:param types.TracebackType tb:
"""
while tb:
try:
tb.tb_frame.clear()
except RuntimeError:
pass
else:
# Using this code triggers that the ref actually goes out of scope, otherwise it does not!
# https://github.com/python/cpython/issues/113939
tb.tb_frame.f_locals # noqa
tb = tb.tb_next Instead of Whenever |
I investigated this a bit. Normally, the
So, why does The reason is, once we access Note, it will go out of scope when it returns from I'm not sure how to fix this. Some options:
|
Code for that: diff --git a/Python/bytecodes.c b/Python/bytecodes.c
index f53ddae8df..51333fb1eb 100644
--- a/Python/bytecodes.c
+++ b/Python/bytecodes.c
@@ -1508,6 +1508,18 @@ dummy_func(
PyObject *v = GETLOCAL(oparg);
ERROR_IF(v == NULL, unbound_local_error);
SETLOCAL(oparg, NULL);
+ PyObject *ns = LOCALS();
+ if(ns) {
+ PyObject *name = GETITEM(_PyFrame_GetCode(frame)->co_localsplusnames, oparg);
+ int err = PyObject_DelItem(ns, name);
+ // Can't use ERROR_IF here.
+ if (err != 0) {
+ _PyEval_FormatExcCheckArg(tstate, PyExc_NameError,
+ NAME_ERROR_MSG,
+ name);
+ GOTO_ERROR(error);
+ }
+ }
}
inst(MAKE_CELL, (--)) { This fixes the problem. Then But I'm not sure if you would want this change? |
A much simpler change for diff --git a/Python/bytecodes.c b/Python/bytecodes.c
index f53ddae8df..7d6c0b41e3 100644
--- a/Python/bytecodes.c
+++ b/Python/bytecodes.c
@@ -1508,6 +1508,7 @@ dummy_func(
PyObject *v = GETLOCAL(oparg);
ERROR_IF(v == NULL, unbound_local_error);
SETLOCAL(oparg, NULL);
+ Py_CLEAR(LOCALS());
}
inst(MAKE_CELL, (--)) { |
Maybe @iritkatriel can help with this? |
Note, in the PR (#113940), I also have added two test cases now, for both of the problems described here ( |
Thanks for merging. Now, one part of this issue is fixed, namely However, the other part still remains open, where locals are potentially not freed even when they get out of scope, e.g. the exception object including its traceback with all frames. Should we open a new separate issue about that? Or reopen this one here? Or not needed, as we already have PEP-667 and PEP-558? #74929 is also similar/related, but describes a more complex scenario. For reference, this is a test case for this problem: class LocalsTest(unittest.TestCase):
"""
Tests for locals.
"""
def test_locals_cleared_after_exception_handled(self):
# see gh-113939
class C:
pass
wr = None
def inner():
nonlocal wr
c = C()
wr = weakref.ref(c)
1/0
try:
inner()
except ZeroDivisionError as exc:
support.gc_collect()
self.assertIsNotNone(wr())
print(exc.__traceback__.tb_frame.f_locals)
support.gc_collect()
self.assertIsNone(wr()) This test still fails now. |
Maybe add a comment to #74929 with the simpler example? |
Bug report
Bug description:
Running this code gives the following output:
You see that
Obj('a, i=2')
only is deleted at exit.This only happens when the
print_tb
is used before, which will accessf_locals
of each frame.traceback.clear_frames
should have cleared the locals. But as you see from the output, it does not.clear_tb
is basically a copy oftraceback.clear_frames
.The problem goes away if you access
tb.tb_frame.f_locals
after it was cleared (i.e.tb.tb_frame.clear()
was called).Looking at the C code, this is what
tb_frame.clear()
will do:https://github.com/python/cpython/blob/3.12/Objects/frameobject.c#L933-L946
However, if you accessed
tb_frame.f_locals
before, it will have created a dictionary inframe->f_locals
here: https://github.com/python/cpython/blob/5c238225f60c33cf1931b1a8c9a3310192c716ae/Objects/frameobject.c#L1218C18-L1218C33That
frame->f_locals
dict will also have references to all the local vars. And thatf_locals
dict is not cleared intb_frame.clear()
.However, then when you access
tb_frame.f_locals
again, it will update the existingframe->f_locals
dict, and delete all the local vars in it, because they are not available anymore. Here:https://github.com/python/cpython/blob/3.12/Objects/frameobject.c#L1256C13-L1256C55
I think it's a bug (or at least very unexpected) that
tb_frame.clear()
does not clearframe->f_locals
.So my suggestion would be to add
Py_CLEAR(f->f_frame->f_locals)
inframe_tp_clear
.There is then another related issue: When the
except
block is left, the exception goes out of scope, so then it should free all the locals (even whenframe.clear()
was not called). However, this is also not the case.After inspecting this further: Once
frame.f_locals
was accessed from the current frame where the exception is handled, thisframe.f_locals
still has a reference to the exception, and thus to the frames, even though theDELETE_FAST
for the exception deleted it from the fast locals. See the comments below for more on this.Note, for PyTorch and others, when you first do extended exception reporting which accesses
f_locals
in any way, this here fixes two arising problems. Related:E.g., this came up for us because we have this extended exception reporting, which accesses
f_locals
:The normal
traceback.clear_frames
here does not help.CPython versions tested on:
3.11, 3.12, 3.13
Operating systems tested on:
Linux
Linked PRs
The text was updated successfully, but these errors were encountered: