Skip to content

Python garbage collector is not removing instances created using cached_property even if the parent class is deleted #136735

@FooKaDoO

Description

@FooKaDoO

Bug report

Bug description:

When instantiating objects through a method with functools.cached_property decorator, sometimes the objects won't be cleaned by the garbage collector. More specifically, when the method is called by iterating a list of objects. It is easier to understand from the reproduction code. This does not happen, when the class object is being cached (some_other_method). Additionally, it does not appear, when the instance in the list/dictionary is being directly accessed (third run), which means one or the other must be an unintended side-effect (that the direct access gets garbage collected or the iterative access does not). In the final run, trying to deleted the lists does not remove the SomeOtherClass instances. The original SomeClass objects do not get removed either. (The final del is to test, if deleting the dictionaries triggers the garbage collector somehow)

Code to reproduce (objgraph has to be installed; can be done with pip install objgraph):

from functools import cached_property
import objgraph
import gc

class SomeOtherClass:
    pass

class SomeClass:
    @cached_property
    def some_method(self):
        return SomeOtherClass()

    @cached_property
    def some_other_method(self):
        return SomeOtherClass

print("First run:")
e1_list = {0: SomeClass()}
for e1 in e1_list.values():
    e1.some_method
e1_list.clear()
gc.collect()
print("SomeClass:", objgraph.by_type('SomeClass'))
print("SomeOtherClass:", objgraph.by_type('SomeOtherClass'))
print()

print("Second run:")
e2_list = {0: SomeClass()}
for e2 in e2_list.values():
    e2.some_method
e2_list.clear()
gc.collect()
print("SomeClass:", objgraph.by_type('SomeClass'))
print("SomeOtherClass:", objgraph.by_type('SomeOtherClass'))
print()

print("Third run (no loop):")
e3_list = {0: SomeClass()}
e3_list[0].some_method
e3_list.clear()
gc.collect()
print("SomeClass:", objgraph.by_type('SomeClass'))
print("SomeOtherClass:", objgraph.by_type('SomeOtherClass'))
print()

print("Fourth run (some_other_method):")
e4_list = {0: SomeClass()}
for e4 in e4_list.values():
    e4.some_other_method()
e4_list.clear()
gc.collect()
print("SomeClass:", objgraph.by_type('SomeClass'))
print("SomeOtherClass:", objgraph.by_type('SomeOtherClass'))
print()

print("Fifth run (Trying to clean up using del):")
del e1_list
del e2_list
del e3_list
del e4_list
gc.collect()
print("SomeClass:", objgraph.by_type('SomeClass'))
print("SomeOtherClass:", objgraph.by_type('SomeOtherClass'))

Output:

First run:
SomeClass: [<__main__.SomeClass object at 0x7153db48d6a0>]
SomeOtherClass: [<__main__.SomeOtherClass object at 0x7153db48f0e0>]

Second run:
SomeClass: [<__main__.SomeClass object at 0x7153db48d6a0>, <__main__.SomeClass object at 0x7153db2f4690>]
SomeOtherClass: [<__main__.SomeOtherClass object at 0x7153db48f0e0>, <__main__.SomeOtherClass object at 0x7153db2f47d0>]

Third run (no loop):
SomeClass: [<__main__.SomeClass object at 0x7153db48d6a0>, <__main__.SomeClass object at 0x7153db2f4690>]
SomeOtherClass: [<__main__.SomeOtherClass object at 0x7153db48f0e0>, <__main__.SomeOtherClass object at 0x7153db2f47d0>]

Fourth run (some_other_method):
SomeClass: [<__main__.SomeClass object at 0x7153db48d6a0>, <__main__.SomeClass object at 0x7153db2f4690>, <__main__.SomeClass object at 0x7153db4d09d0>]
SomeOtherClass: [<__main__.SomeOtherClass object at 0x7153db48f0e0>, <__main__.SomeOtherClass object at 0x7153db2f47d0>]

Fifth run (Trying to clean up using del):
SomeClass: [<__main__.SomeClass object at 0x7153db48d6a0>, <__main__.SomeClass object at 0x7153db2f4690>, <__main__.SomeClass object at 0x7153db4d09d0>]
SomeOtherClass: [<__main__.SomeOtherClass object at 0x7153db48f0e0>, <__main__.SomeOtherClass object at 0x7153db2f47d0>]

CPython versions tested on:

CPython main branch, 3.15, 3.14, 3.13, 3.10

Operating systems tested on:

Linux

Metadata

Metadata

Assignees

No one assigned

    Labels

    pendingThe issue will be closed if no feedback is providedtype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions