Your app keeps a registry of open sessions, so that a debug page can list them. The registry should find each session while it is in use, but it should not be the reason a finished session stays in memory. A plain dictionary holds a reference to each value, so every session you register stays alive until you remove it.
A weak reference is a reference that does not keep its object alive.
Calling it returns the object, or None once the object is freed:
import gc
import weakref
class Session:
def __init__(self, user: str) -> None:
self.user = user
session = Session("ana")
ref = weakref.ref(session)
print(ref() is session) # True
del session
gc.collect()
print(ref()) # None
del session removes the last ordinary reference, so the reference count
reaches zero and CPython frees the object at once. The weak reference does
not add to the count. Here gc.collect() has nothing left to do. It is
there in case a cycle still held the object.
Call the weak reference once
Call ref() once, store the result in a local variable, and use the local:
current = ref()
if current is not None:
print(current.user)
The local is an ordinary reference, so CPython cannot free the object
while you use it. If you call ref() twice, once to check and once to
use, another thread can drop the last reference in between. mypy rejects
the double call too. ref has the type weakref.ref[Session], so each
call gives Session | None, and mypy allows .user only on a value that
you have checked.
Use a weak container
The weakref module has containers that store weak references for you:
WeakValueDictionaryholds its values weakly and removes an entry when its value is freed.WeakKeyDictionaryholds its keys weakly. Its values are ordinary references, so a value that points back at its key keeps the key alive.WeakSetholds its members weakly.
A weak container is not a cache. It finds an object only while some other code still holds it. To keep results around for the next request, use a real cache with a size limit.
A method cache can keep instances alive
functools.lru_cache keeps a reference to every argument it caches. For a
method, the first argument is self:
import gc
import weakref
from functools import lru_cache
class ReportBuilder:
@lru_cache(maxsize=2)
def render(self, template: str) -> str:
return template.upper()
builder = ReportBuilder()
ref = weakref.ref(builder)
builder.render("summary")
del builder
gc.collect()
print(ref() is None) # False, the cache still holds the builder
ReportBuilder.render.cache_clear()
gc.collect()
print(ref() is None) # True
All instances share one cache for the method. A maxsize of 2 keeps at
most two entries, and the cache lets go of an old builder once newer
calls push its entries out. Without an argument, lru_cache keeps 128
entries, and functools.cache keeps every entry until you clear it. If
each result belongs to one instance, functools.cached_property stores it
on the instance instead, and it goes away with the instance.
Check that your class supports weak references
You cannot create a weak reference to a list, dict, str, int or
tuple. You can create one to an instance of an ordinary class. A
dataclass with slots=True cannot have one, unless you also pass
weakref_slot=True. Python checks when you create the weak reference, and
raises TypeError if the class has no room for one. So test
weakref.ref(instance) on the real class, and keep that test if a
registry depends on it.
Check your understanding
A registry stores weakref.ref(obj) in a dictionary. After the caller drops its last reference to obj, what does len(registry) report?
Show answer
Hide answer
It depends on how __len__ counts. The dictionary still holds the dead
weak reference, so len(self._refs) still counts it. Count only the
references that return an object. Or use a WeakValueDictionary, which
removes dead entries for you.
Going further
weakref.finalize(obj, func)runsfuncwhenobjis freed. Do not pass a bound method ofobjasfunc, because it would keepobjalive. Close files and connections with awithblock instead, so you know when it happens.- A weak reference to a bound method, such as
weakref.ref(obj.method), dies at once, because Python makes a new method object on each access. Useweakref.WeakMethodfor callbacks. - The weakref documentation lists the built-in types that support weak references.