Skip to content

Weak Reference Patterns

Mental Model

Weak references let you observe an object without voting to keep it alive. In patterns like observer or cache, this is critical: you want to know about an object while it exists, but you don't want your reference to be the reason it can never be garbage collected. WeakSet and WeakValueDictionary are the standard tools for this.

Observer Pattern

The observer pattern often creates memory leaks because observables hold strong references to observers. Use WeakSet to allow observers to be garbage collected when no longer needed elsewhere.

Problem: Strong References

```python class Observable: def init(self): self._observers = set() # Strong references

def subscribe(self, observer):
    self._observers.add(observer)

def notify(self):
    for obs in self._observers:
        obs.update()

Problem: observers never garbage collected

even when no other references exist

```

Solution: WeakSet

```python import weakref

class Observable: def init(self): self._observers = weakref.WeakSet()

def subscribe(self, observer):
    self._observers.add(observer)

def unsubscribe(self, observer):
    self._observers.discard(observer)

def notify(self):
    # Dead observers automatically removed
    for obs in self._observers:
        obs.update()

Usage

class Observer: def update(self): print("Notified!")

subject = Observable() obs = Observer() subject.subscribe(obs)

subject.notify() # "Notified!"

del obs # Observer can now be garbage collected subject.notify() # Nothing happens — observer is gone ```


Self-Cleaning Cache

Caches can consume unbounded memory. Use WeakValueDictionary to automatically evict entries when cached objects are no longer referenced elsewhere.

Problem: Unbounded Cache

```python cache = {}

def get_object(key): if key not in cache: obj = create_expensive_object(key) cache[key] = obj # Stays forever! return cache[key]

Cache grows without bound

```

Solution: WeakValueDictionary

```python import weakref

cache = weakref.WeakValueDictionary()

def get_object(key): obj = cache.get(key) if obj is None: obj = create_expensive_object(key) cache[key] = obj return obj

When no other references to obj exist,

it's automatically removed from cache

```

Example: Image Cache

```python import weakref

class ImageCache: def init(self): self._cache = weakref.WeakValueDictionary()

def get_image(self, path):
    image = self._cache.get(path)
    if image is None:
        image = load_image(path)  # Expensive
        self._cache[path] = image
    return image

Images are cached while in use

Automatically evicted when no longer referenced

```


Parent-Child Relationships

Bidirectional relationships create reference cycles. Use weak references for back-references (child → parent) to allow garbage collection.

Problem: Reference Cycle

```python class Parent: def init(self): self.children = []

def add_child(self, child):
    self.children.append(child)
    child.parent = self  # Strong back-reference

class Child: def init(self): self.parent = None # Creates cycle when set

Cycle: parent -> child -> parent

Requires cycle GC, delays cleanup

```

Solution: Weak Back-Reference

```python import weakref

class Parent: def init(self): self.children = []

def add_child(self, child):
    self.children.append(child)
    child.set_parent(self)

class Child: def init(self): self._parent_ref = None

def set_parent(self, parent):
    self._parent_ref = weakref.ref(parent)

@property
def parent(self):
    if self._parent_ref is None:
        return None
    return self._parent_ref()  # Returns None if parent is dead

No cycle: parent -> child -> weak_ref

Parent can be garbage collected immediately

```

Tree Structure Example

```python import weakref

class TreeNode: def init(self, value): self.value = value self.children = [] self._parent_ref = None

def add_child(self, child):
    self.children.append(child)
    child._parent_ref = weakref.ref(self)

@property
def parent(self):
    if self._parent_ref is None:
        return None
    return self._parent_ref()

def path_to_root(self):
    path = [self.value]
    node = self.parent
    while node is not None:
        path.append(node.value)
        node = node.parent
    return path[::-1]

Usage

root = TreeNode("root") child = TreeNode("child") grandchild = TreeNode("grandchild")

root.add_child(child) child.add_child(grandchild)

print(grandchild.path_to_root()) # ['root', 'child', 'grandchild'] ```


Summary

Pattern Weak Reference Type Use Case
Observer WeakSet Allow observers to be GC'd without explicit unsubscribe
Cache WeakValueDictionary Auto-evict cached objects when no longer used
Parent-Child weakref.ref() Break cycles in bidirectional relationships

Key Principle: Use strong references for ownership (parent → child), weak references for back-references or non-owning relationships (child → parent, cache → cached object, observable → observer).


Exercises

Exercise 1. Implement an EventBus class that uses weakref.WeakSet to store subscribers. Write a test that subscribes 5 observer objects, deletes 3 of them, and then calls notify(). Print how many observers were actually notified and verify it equals 2.

Solution to Exercise 1
```python
import weakref
import gc

class EventBus:
    def __init__(self):
        self._subscribers = weakref.WeakSet()

    def subscribe(self, obj):
        self._subscribers.add(obj)

    def notify(self):
        count = 0
        for sub in self._subscribers:
            sub.on_event()
            count += 1
        return count

class Subscriber:
    def __init__(self, name):
        self.name = name
    def on_event(self):
        print(f"  {self.name} notified")

bus = EventBus()
subs = [Subscriber(f"S{i}") for i in range(5)]
for s in subs:
    bus.subscribe(s)

# Delete 3 subscribers
del subs[0], subs[1], subs[2]
gc.collect()

notified = bus.notify()
print(f"Notified: {notified}")  # 2
```

Exercise 2. Build an ObjectCache class backed by weakref.WeakValueDictionary. Write a test that inserts 10 objects, keeps strong references to only 3, and then triggers garbage collection. Print the cache length before and after to show automatic eviction.

Solution to Exercise 2
```python
import weakref
import gc

class ObjectCache:
    def __init__(self):
        self._cache = weakref.WeakValueDictionary()

    def put(self, key, value):
        self._cache[key] = value

    def __len__(self):
        return len(self._cache)

class Heavy:
    def __init__(self, n):
        self.data = list(range(n))

cache = ObjectCache()
strong_refs = []

for i in range(10):
    obj = Heavy(100)
    cache.put(f"key_{i}", obj)
    if i < 3:
        strong_refs.append(obj)

print(f"Before GC: {len(cache)} entries")  # 10
gc.collect()
print(f"After GC:  {len(cache)} entries")  # 3
```

Exercise 3. Create a TreeNode class with a children list (strong references) and a parent property backed by weakref.ref. Build a tree of depth 4, then delete the root. Use weakref.ref callbacks to print a message when each node is collected, and verify that all nodes are freed.

Solution to Exercise 3
```python
import weakref
import gc

class TreeNode:
    def __init__(self, value):
        self.value = value
        self.children = []
        self._parent_ref = None

    def add_child(self, child):
        self.children.append(child)
        child._parent_ref = weakref.ref(self)

    @property
    def parent(self):
        if self._parent_ref is None:
            return None
        return self._parent_ref()

collected = []

def on_collect(ref):
    collected.append(ref)

# Build tree of depth 4
root = TreeNode("root")
refs = [weakref.ref(root, on_collect)]

current_level = [root]
for depth in range(1, 4):
    next_level = []
    for parent in current_level:
        for i in range(2):
            child = TreeNode(f"d{depth}-{i}")
            parent.add_child(child)
            refs.append(weakref.ref(child, on_collect))
            next_level.append(child)
    current_level = next_level

total_nodes = len(refs)
del root, current_level, next_level, child, parent
gc.collect()

print(f"Total nodes: {total_nodes}")
print(f"Collected:   {len(collected)}")
print(f"All freed:   {len(collected) == total_nodes}")
```