Bug report
Bug description:
Objects can't be garbage collected after accessing __dict__. The issue was noticed on a class with many cached_property which store the cache entries by directly accessing the instance __dict__.
If cached_property is modified by removing this line, the issue goes away:
https://github.com/python/cpython/blob/3.13/Lib/functools.py#L1028
The attached reproduction creates instances of two classes and checks if they are freed or not.
Reproduction of the issue with the attached pytest test cases:
mkdir cached_property_issue
python3 -m venv .venv
source .venv/bin/activate
pip3 install pytest
pytest test_cached_property_issue.py
from __future__ importannotationsfromcollections.abcimportGeneratorfromfunctoolsimportcached_propertyimportgcfromtypingimportAny, Selfimportweakrefimportpytesthass_instances: list[weakref.ref[HackHomeAssistant]] = []
sensor_instances: list[weakref.ReferenceType[HackEntity]] = []
@pytest.fixture(autouse=True, scope="module")defgarbage_collection() ->Generator[None]:
"""Run garbage collection and check instances."""yieldgc.collect()
assert [bool(obj()) forobjinhass_instances] == [False, True]
assert [bool(obj()) forobjinsensor_instances] == [False, True]
classHackEntity:
"""A class with many properties."""entity_id: str|None=Noneplatform=Noneregistry_entry=None_unsub_device_updates=None@cached_propertydefshould_poll(self) ->bool:
returnFalse@cached_propertydefunique_id(self) ->None:
returnNone@cached_propertydefhas_entity_name(self) ->bool:
returnFalse@cached_propertydef_device_class_name(self) ->None:
returnNone@cached_propertydef_unit_of_measurement_translation_key(self) ->None:
returnNone@propertydefname(self) ->None:
returnNone@cached_propertydefcapability_attributes(self) ->None:
returnNone@cached_propertydefstate_attributes(self) ->None:
returnNone@cached_propertydefextra_state_attributes(self) ->None:
returnNone@cached_propertydefdevice_class(self) ->None:
returnNone@cached_propertydefunit_of_measurement(self) ->None:
returnNone@cached_propertydeficon(self) ->None:
returnNone@cached_propertydefentity_picture(self) ->None:
returnNone@cached_propertydefavailable(self) ->bool:
returnTrue@cached_propertydefassumed_state(self) ->bool:
returnFalse@cached_propertydefforce_update(self) ->bool:
returnFalse@cached_propertydefsupported_features(self) ->None:
returnNone@cached_propertydefattribution(self) ->None:
returnNone@cached_propertydeftranslation_key(self) ->None:
returnNone@cached_propertydefoptions(self) ->None:
returnNone@cached_propertydefstate_class(self) ->None:
returnNone@cached_propertydefnative_unit_of_measurement(self) ->None:
returnNone@cached_propertydefsuggested_unit_of_measurement(self) ->None:
returnNone@propertydefstate(self) ->Any:
self._unit_of_measurement_translation_keyself.native_unit_of_measurementself.optionsself.state_classself.suggested_unit_of_measurementself.translation_keyreturnNonedefasync_write_ha_state(self) ->None:
self._verified_state_writable=Trueself.capability_attributesself.availableself.stateself.extra_state_attributesself.state_attributesself.unit_of_measurementself.assumed_stateself.attributionself.device_classself.entity_pictureself.iconself._device_class_nameself.has_entity_nameself.supported_featuresself.force_updatedefasync_on_remove(self) ->None:
self._on_remove= []
defadd_to_platform_start(
self, hass: HackHomeAssistant, platform: HackEntityPlatform
) ->None:
self.hass=hassself.platform=platformclassCompensationSensor(HackEntity):
"""This concrete class won't be garbage collected."""def__init__(self) ->None:
"""Initialize the Compensation sensor."""self.__dict__sensor_instances.append(weakref.ref(self))
self.parallel_updates=Noneself._platform_state=Noneself._state_info= {}
self.async_write_ha_state()
classCompensationSensor2(HackEntity):
"""This concrete class will be garbage collected."""def__init__(self) ->None:
"""Initialize the Compensation sensor."""sensor_instances.append(weakref.ref(self))
self.parallel_updates=Noneself._platform_state=Noneself._state_info= {}
self.async_write_ha_state()
classHackHomeAssistant:
"""Root object of the Home Assistant home automation."""def__new__(cls) ->Self:
"""Set the _hass thread local data."""hass=super().__new__(cls)
hass_instances.append(weakref.ref(hass))
returnhassclassHackEntityPlatform:
"""Manage the entities for a single platform."""def__init__(
self,
hass: HackHomeAssistant,
) ->None:
"""Initialize the entity platform."""self.hass=hassself.entities: dict[str, HackEntity] = {}
defasync_add_entity(
self,
new_entity: HackEntity,
) ->None:
"""Add entities for a single platform async."""entity=new_entityentity.add_to_platform_start(self.hass, self)
entity.unique_identity.entity_id="sensor.test"entity_id=entity.entity_idself.entities[entity_id] =entityentity.async_on_remove()
entity.should_polldeftest_limits1() ->None:
"""Create an object which will be garbage collected."""hass=HackHomeAssistant()
sens=CompensationSensor2()
entity_platform=HackEntityPlatform(hass)
entity_platform.async_add_entity(sens)
deftest_limits2() ->None:
"""Create an object which won't be garbage collected."""hass=HackHomeAssistant()
sens=CompensationSensor()
entity_platform=HackEntityPlatform(hass)
entity_platform.async_add_entity(sens)
deftest_limits3() ->None:
"""Last test, garbage collection check runs after this test."""CPython versions tested on:
3.13
Operating systems tested on:
Linux
Linked PRs
Bug report
Bug description:
Objects can't be garbage collected after accessing
__dict__. The issue was noticed on a class with manycached_propertywhich store the cache entries by directly accessing the instance__dict__.If
cached_propertyis modified by removing this line, the issue goes away:https://github.com/python/cpython/blob/3.13/Lib/functools.py#L1028
The attached reproduction creates instances of two classes and checks if they are freed or not.
Reproduction of the issue with the attached pytest test cases:
CPython versions tested on:
3.13
Operating systems tested on:
Linux
Linked PRs