Weak References

A weak reference lets you refer to an object without keeping it alive — the object can still be garbage-collected as if that reference didn't exist, which is exactly what caches and observer patterns need.

Every ordinary reference in Python — a variable, a list element, an attribute — is a strong reference: as long as it exists, it counts toward keeping the referenced object alive. A weak reference, provided by the standard library's weakref module, is different: it lets you access an object while it's alive, but does not itself keep the object alive, and automatically becomes invalid once the object is reclaimed.

Strong References vs. Weak References

By default, every reference to an object is 'strong': it participates in reachability and, in CPython, in the object's reference count. A weak reference is created explicitly through the weakref module and deliberately does not do this — the object it points to can be freed even while the weak reference to it still technically exists.

Calling a weakref.ref object like a function returns the referenced object if it's still alive, or None if it has already been reclaimed. This 'dereference and check' pattern is central to using weak references safely.

import weakref

class Resource:
    pass

r = Resource()
weak = weakref.ref(r)     # a weak reference to r; does NOT keep r alive

print(weak())              # <Resource object at ...> — call it to get the object
del r                      # r's only strong reference is gone
print(weak())              # None — the object was reclaimed; the weak ref reports it

Why Weak References Exist

Weak references solve a specific, recurring problem: you want to keep track of an object — for a cache, a registry, an observer list — without that tracking itself becoming a reason the object never gets garbage-collected. A plain dict or list used for this purpose silently keeps every entry alive forever (see the Memory Management topic's discussion of leaks-as-retained-references); weak references let the tracking structure step out of the way of normal reclamation.

Not Every Object Supports Weak References

CPython implementation detail: weak-referenceability depends on the object's type reserving a __weakref__ slot. Most user-defined classes support it automatically; several built-in immutable types do not, because CPython doesn't allocate the extra slot for them by default.

See the __slots__ topic: a slotted class needs '__weakref__' explicitly listed among its slots to support weak references, since slots otherwise omit that hidden storage entirely.

import weakref

class Widget:
    pass

w = Widget()
weakref.ref(w)          # works fine

# weakref.ref([1, 2, 3])   # TypeError: cannot create weak reference to 'list' object
# weakref.ref(42)           # TypeError: cannot create weak reference to 'int' object

class SlottedNoWeakref:
    __slots__ = ("x",)

s = SlottedNoWeakref()
# weakref.ref(s)  # TypeError -- must add "__weakref__" to __slots__ explicitly

Weakref Callbacks

weakref.ref() optionally accepts a callback function, invoked automatically (with the now-dead weak reference as its argument) at the moment the referenced object is actually reclaimed — useful for cleanup logic tied to an object's disappearance.

For simpler one-shot cleanup tied to object finalization, weakref.finalize() is usually preferable — it's more robust (it fires exactly once, and can be cancelled) than a bare callback.

import weakref

class Resource:
    pass

def on_finalized(weak_ref):
    print("Resource was garbage-collected")

r = Resource()
weak = weakref.ref(r, on_finalized)
del r
# "Resource was garbage-collected" prints here (in the common,
# non-cyclic CPython case, this happens promptly)
import weakref

class Resource:
    pass

r = Resource()
finalizer = weakref.finalize(r, print, "cleaned up!")
del r    # prints "cleaned up!" once the object is actually reclaimed

WeakValueDictionary: Values Held Weakly

weakref.WeakValueDictionary behaves like a normal dict, except its values are held weakly: an entry disappears automatically once nothing else strongly references its value, without you ever having to remove it by hand. This is the textbook tool for an object cache that should not keep unused objects alive forever.

import weakref

class ExpensiveObject:
    def __init__(self, key):
        self.key = key

cache: weakref.WeakValueDictionary = weakref.WeakValueDictionary()

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

obj = get_object("a")
print(len(cache))     # 1
del obj                # last strong reference gone
print(len(cache))      # 0 -- entry was automatically removed

WeakKeyDictionary: Keys Held Weakly

weakref.WeakKeyDictionary is the mirror image: keys are held weakly instead of values. It's the standard way to attach extra data to an object externally (a form of 'out-of-band' attribute) without preventing that object from being garbage-collected when nothing else needs it.

import weakref

class Session:
    pass

extra_data: weakref.WeakKeyDictionary = weakref.WeakKeyDictionary()

session = Session()
extra_data[session] = {"logged_in_at": "10:32"}

print(extra_data[session])   # {'logged_in_at': '10:32'}
del session
# the WeakKeyDictionary entry is automatically dropped once the
# Session object itself is reclaimed -- no manual cleanup needed

Caches and the Observer Pattern

Two of the most common real-world motivations for weak references are caching (already shown above) and observer/event patterns, where a subject object holds a list of listener callbacks or objects that shouldn't be kept alive merely because they're subscribed.

Without weak references here, EventBus._listeners would hold a strong reference to every subscriber forever, keeping them all alive for the lifetime of the bus regardless of whether anything else in the program still cares about them — exactly the retained-reference leak pattern from the Memory Management topic.

import weakref

class EventBus:
    def __init__(self):
        self._listeners: list[weakref.ReferenceType] = []

    def subscribe(self, listener):
        self._listeners.append(weakref.ref(listener))

    def publish(self, event):
        # clean up dead references while notifying live ones
        alive = []
        for ref in self._listeners:
            listener = ref()
            if listener is not None:
                listener.handle(event)
                alive.append(ref)
        self._listeners = alive

class Listener:
    def handle(self, event):
        print(f"received: {event}")

bus = EventBus()
listener = Listener()
bus.subscribe(listener)
bus.publish("startup")     # received: startup

del listener
bus.publish("shutdown")     # nothing prints -- listener is gone, and
                             # was cleanly dropped from the list too

weakref.proxy: A Transparent Weak Handle

weakref.proxy() returns an object that behaves like the original (attribute access, method calls, etc. are forwarded transparently) rather than requiring you to call it like weakref.ref() does. Accessing a proxy after its target is gone raises ReferenceError instead of silently returning None.

import weakref

class Widget:
    def describe(self):
        return "a widget"

w = Widget()
p = weakref.proxy(w)
print(p.describe())    # "a widget" -- looks and behaves like w itself

del w
try:
    p.describe()
except ReferenceError:
    print("the referenced object no longer exists")

Mental Model

Think of a strong reference as physically holding onto something — as long as you're holding it, it can't be thrown away. A weak reference is like keeping a sticky note with someone's address on it: it doesn't stop them from moving away, and if you check the address after they've moved, you simply find nothing there. WeakValueDictionary and WeakKeyDictionary are drawers full of such sticky notes that quietly remove themselves the instant the address they point to becomes stale.

Quick Reference

weakref.ref(obj[, callback])   -> callable weak reference; call it to get
                                   obj or None if obj is gone
weakref.proxy(obj)              -> transparent weak handle; raises
                                   ReferenceError once obj is gone
weakref.finalize(obj, func, *a) -> run func(*a) once when obj is reclaimed
weakref.WeakValueDictionary()   -> dict whose VALUES are held weakly
weakref.WeakKeyDictionary()      -> dict whose KEYS are held weakly
Requires the target type to support __weakref__ (most user classes do;
many builtins like list/dict/int do not; slotted classes need
"__weakref__" added to __slots__ explicitly)

Key Takeaways

A weak reference lets you refer to an object without keeping it alive; it does not count toward the object's reachability.

weakref.ref(obj) returns a callable that gives back the object while alive, or None after it's reclaimed; weakref.proxy(obj) gives a transparent handle that raises ReferenceError instead.

Not every type supports weak references — many built-ins (list, dict, int, str) don't by default, and slotted classes need '__weakref__' added to __slots__ explicitly.

WeakValueDictionary and WeakKeyDictionary automatically drop entries once their weakly-held side becomes unreachable, making them the standard tool for caches and per-object side-tables.

Weak references are the idiomatic fix for observer/subscriber lists and back-pointers in cyclic structures (like parent pointers in a tree) that would otherwise keep objects alive indefinitely or rely on the cyclic collector unnecessarily.

import weakref cache = {} def get(key, factory): if key not in cache: cache[key] = factory() return cache[key] # a plain dict keeps every cached value alive forever import weakref cache: weakref.WeakValueDictionary = weakref.WeakValueDictionary() def get(key, factory): obj = cache.get(key) if obj is None: obj = factory() cache[key] = obj return obj # entries disappear automatically once nothing else references the value

A plain dict used as a cache holds strong references to every value forever, defeating garbage collection; WeakValueDictionary lets unused cached objects be reclaimed automatically.

import weakref r = SomeObject() weak = weakref.ref(r) print(weak.value) # AttributeError -- weakref.ref is not the object itself import weakref r = SomeObject() weak = weakref.ref(r) obj = weak() # call it to dereference if obj is not None: print(obj.value)

A weakref.ref object must be called (weak()) to retrieve the underlying object (or None if it's gone); it does not forward attribute access the way weakref.proxy does.

import weakref proxy = weakref.proxy(some_object) del some_object print(proxy.attribute) # ReferenceError raised, uncaught -- crashes import weakref proxy = weakref.proxy(some_object) del some_object try: print(proxy.attribute) except ReferenceError: print("target object no longer exists")

Unlike weakref.ref(), which returns None quietly, weakref.proxy raises ReferenceError when its target is gone; code using a proxy after the target might be dead must handle that exception.

What is the fundamental difference between a strong reference and a weak reference in terms of object reachability?

A strong reference counts toward keeping the referenced object alive/reachable; a weak reference does not — the object can still be garbage-collected as if the weak reference didn't exist, and the weak reference automatically reflects that the object is gone once it is.

How do you retrieve the underlying object from a weakref.ref instance, and what do you get back if the object has already been reclaimed?

Call the weakref.ref object like a function (weak()); it returns the live object if it still exists, or None if the object has already been garbage-collected.

Why can't you create a weak reference to a plain list, int, or str object directly?

Weak-referenceability depends on the type reserving a __weakref__ slot in its instances; many built-in types (including list, dict, int, str) don't allocate that slot by default, so weakref.ref() on them raises TypeError.

What is the key difference in behavior between weakref.ref(obj) and weakref.proxy(obj) once obj is reclaimed?

weak() on a weakref.ref returns None quietly once the object is gone; accessing any attribute or method on a weakref.proxy after its target is gone raises ReferenceError instead, since a proxy is meant to behave transparently like the object itself.

Why is WeakValueDictionary a better choice than a plain dict for an object cache that shouldn't prevent garbage collection?

A plain dict holds a strong reference to every stored value, keeping all cached objects alive for as long as the dict exists, regardless of whether anything else still needs them; WeakValueDictionary holds its values weakly, so an entry is automatically removed as soon as nothing else strongly references that value, letting unused cache entries be reclaimed naturally.

Give a concrete reason an observer/event-subscriber list should typically hold weak references to its subscribers rather than strong ones.

If the subject (e.g. an EventBus) holds strong references to every subscribed listener, those listeners are kept alive for as long as the subject exists even after the rest of the program has finished with them — a subtle memory leak; holding weak references instead lets listeners be garbage-collected normally once nothing else references them, with dead entries simply dropped from the subscriber list.

Write a WeakValueDictionary-backed object pool get_or_create(key) that reuses an existing live object for a key if one exists, and otherwise creates and caches a new one, demonstrating that entries disappear once the object is no longer referenced elsewhere.

import weakref class Widget: def __init__(self, key): self.key = key _pool: weakref.WeakValueDictionary = weakref.WeakValueDictionary() def get_or_create(key): widget = _pool.get(key) if widget is None: widget = Widget(key) _pool[key] = widget return widget w1 = get_or_create("a") w2 = get_or_create("a") print(w1 is w2) # True -- same cached instance print(len(_pool)) # 1 del w1, w2 print(len(_pool)) # 0 -- automatically cleared once unreferenced

Because _pool holds its values weakly, requesting the same key twice while a live reference exists returns the identical cached Widget; once both external references (w1 and w2) are deleted, nothing strongly references the Widget anymore, so it is reclaimed and WeakValueDictionary automatically drops the now-dead entry, shrinking the pool to zero.

What does each print statement output?

First: True (ref() returns the same object n currently is). Second: None (n has been reclaimed since nothing else referenced it, so the weak reference now reports None). Third: True — a weakref.ref object itself is always truthy regardless of whether its target is alive; bool(ref) does not tell you liveness, only calling ref() and checking the result does.

Write a WeakKeyDictionary example that attaches a 'visit count' to arbitrary objects externally, without modifying their class, and show that the entry disappears once the object itself is garbage-collected.

import weakref class Page: def __init__(self, url): self.url = url visit_counts: weakref.WeakKeyDictionary = weakref.WeakKeyDictionary() def record_visit(page): visit_counts[page] = visit_counts.get(page, 0) + 1 home = Page("/home") record_visit(home) record_visit(home) print(visit_counts[home]) # 2 print(len(visit_counts)) # 1 del home print(len(visit_counts)) # 0 -- entry removed automatically visit_counts uses Page instances themselves as weakly-held keys, so external per-object bookkeeping is possible without adding a new attribute to Page. Once home is deleted and nothing else references that Page instance, the WeakKeyDictionary entry keyed on it is automatically dropped, showing the dictionary never artificially keeps tracked objects alive.

import weakref

class Node:
    pass

n = Node()
ref = weakref.ref(n)
print(ref() is n)
del n
print(ref())
print(bool(ref))