Skip to content
← Back to explainers

Python explainer

Track Objects with Weak References

About 4 min to read. Allow extra time to work through examples.

Let a registry find objects without keeping them alive, see how a method cache can keep instances alive, and check that your class supports weak references.

This article uses Python. You can make it your practice mode to browse related content across the site.

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:

  • WeakValueDictionary holds its values weakly and removes an entry when its value is freed.
  • WeakKeyDictionary holds its keys weakly. Its values are ordinary references, so a value that points back at its key keeps the key alive.
  • WeakSet holds 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

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) runs func when obj is freed. Do not pass a bound method of obj as func, because it would keep obj alive. Close files and connections with a with block 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. Use weakref.WeakMethod for callbacks.
  • The weakref documentation lists the built-in types that support weak references.

step 14 of 38

Memory, Copies and Slow Code

Up next Build a Registry That Does Not Keep Objects Alive