This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: caught and stored exception creates a reference cycle outside its traceback
Type: behavior Stage:
Components: Interpreter Core Versions: Python 3.6
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: mark.dickinson, vojtechfried
Priority: normal Keywords:

Created on 2017-07-24 09:05 by vojtechfried, last changed 2022-04-11 14:58 by admin.

Messages (4)
msg298936 - (view) Author: Vojtěch Fried (vojtechfried) Date: 2017-07-24 09:05
I have this test case:

import gc
import sys
import traceback

def hold_world():
    try:
        raise Exception("test1")
    except Exception as exc:
        print("exc caught in frame: ", exc.__traceback__.tb_frame)
        assert not exc.__traceback__.tb_next
        #exc.__traceback__ = None #ok
        tmp = exc
        traceback.clear_frames(exc.__traceback__) #not enough

def use_obj( o ):
    hold_world()
    #o = None #needed to get rid of the reference in the frame

def main():
    o = ["survivor"]
    print(gc.get_referrers(o))
    print(sys.getrefcount(o)) #2
    use_obj( o )
    print(gc.get_referrers(o))
    print(sys.getrefcount(o)) #3
    #o = None #needed to get rid of the reference in the frame

if __name__ == '__main__':
    main()

The outpus is:
[<frame object at 0x0000000001DA2630>]
2
exc caught in frame:  <frame object at 0x0000000002206928>
[<frame object at 0x0000000001DA2630>, <frame object at 0x0000000002197B08>]
3

When either uncommenting the line "exc.__traceback__ = None" or uncommenting "o = None" lines, the output is like
[<frame object at 0x0000000001DA2630>]
2
exc caught in frame:  <frame object at 0x0000000002256928>
[<frame object at 0x0000000001DA2630>]
2

It seems that "hold_world" function somehow manages to (indirectly) add a reference to "o" object. So "o" is not cleared at "main" end, but rather garbage collected.
Even though there is a reference cycle tmp -> traceback -> frame -> tmp, the frames outside "hold_world" should not be affected, but it looks like they are.
msg298961 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2017-07-24 12:27
It's the f_back reference from the inner frame that's keeping the outer frame alive.

Here's a picture of the create garbage: http://imgur.com/a/OCRe3

And here's the script that created it:


import gc
import refcycle
import sys
import traceback

def hold_world():
    try:
        raise Exception("test1")
    except Exception as exc:
        tmp = exc

def use_obj( o ):
    hold_world()

def main():
    gc.disable()
    gc.collect()
    o = ["survivor"]
    use_obj( o )
    garbage = refcycle.garbage()
    garbage.export_image()


if __name__ == '__main__':
    main()
msg298962 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2017-07-24 12:31
BTW, I don't think that this counts as a Python bug: Python is behaving as designed here. Not closing just yet, though: it may be that there's an opportunity for improved documentation, or a wider discussion on how to solve such issues in general.
msg298964 - (view) Author: Vojtěch Fried (vojtechfried) Date: 2017-07-24 12:59
Ok. As an input for a potential discussion:

Should all cases in the std library that store an exception be reported as a bug (because it can potentially mess with reference counting?) One such case is function create_connection in socket.py.

If "hold_world" is a function in an external code and the reference cycle problem only appears in an exceptional case it is hard to debug and it can hardly be easily fixed.

If "hold_world" is in an external code, it may be impossible to deal with. You can only diligently set to None or del in each function on the stack. But if you have a factory function that actually produces an object you don't want to be garbage collected, you can't set the reference to None before returning it.

It is surprising that the object is affected by a function that does not know about it. It is not stored globally, it is not passed to it. It just happens to be at a wrong time at a wrong place.

Should traceback.clear_frames clear the "back referenced" frames?
History
Date User Action Args
2022-04-11 14:58:49adminsetgithub: 75188
2017-07-24 12:59:36vojtechfriedsetmessages: + msg298964
2017-07-24 12:31:22mark.dickinsonsetmessages: + msg298962
2017-07-24 12:27:08mark.dickinsonsetnosy: + mark.dickinson
messages: + msg298961
2017-07-24 09:05:19vojtechfriedcreate