backend / python core / 29_weakref.md

weakref — References That Don't Prevent GC

8 interview angles 8 min read source

weakref — References That Don’t Prevent GC

A weakref references an object without incrementing its refcount. The object can be collected while a weak reference still exists; the weakref then “goes dead” and dereferencing it returns None. Used for caches, observer patterns, parent-child links, and any case where “I want to know about this object, but I shouldn’t keep it alive.”

For Python’s reference counting and GC see 27_memory_model.md. For memory-related optimization see 28_slots.md.

The mental model

Normal references (strong):

strong_ref → [object]   # object's refcount += 1; can't be collected while ref exists

Weak references:

weak_ref ┄┄> [object]   # object's refcount UNCHANGED; can be collected anytime
                         # when collected, weak_ref dereferences to None

Basic usage

import weakref

class Document:
    pass

doc = Document()
ref = weakref.ref(doc)

print(ref())     # <Document object>  — call the ref to get the object
print(doc is ref())  # True

del doc          # last strong reference gone → object collected
print(ref())     # None — the weakref is now "dead"

Calling the weak reference like a function returns either the object (still alive) or None (collected).

For attribute-like access, use weakref.proxy:

proxy = weakref.proxy(doc)
proxy.name = "foo"      # passes through to the underlying object
# But if doc is collected:
# ReferenceError: weakly-referenced object no longer exists

proxy() looks like the object; raises ReferenceError if the underlying was collected. Less common than ref().

WeakValueDictionary — the canonical cache

A dict where values are weakly referenced; entries vanish when their values are GC’d:

import weakref

cache = weakref.WeakValueDictionary()

class User:
    def __init__(self, id):
        self.id = id

u = User(42)
cache[42] = u

print(cache[42])      # <User 42>

del u                  # only strong reference gone
print(42 in cache)     # False — entry auto-removed

Use when:

  • You want a cache that doesn’t keep entries alive longer than needed.
  • Same object is accessed often by ID; you’d waste memory re-loading.
  • Lifetime management is handled by other parts of the code.

@functools.cache is a strong cache — entries live forever. WeakValueDictionary is the “weak cache” alternative.

WeakKeyDictionary — per-object metadata

metadata = weakref.WeakKeyDictionary()

class Document:
    pass

doc = Document()
metadata[doc] = {"views": 0, "last_seen": now()}

# Use metadata[doc] anywhere

del doc                # doc collected; metadata entry vanishes automatically

Use case: attach data to objects without modifying their class:

# Track which functions a thread has called
function_calls = weakref.WeakKeyDictionary()

def record(thread, fn_name):
    function_calls.setdefault(thread, []).append(fn_name)

When the thread dies, its entries disappear. No manual cleanup needed.

WeakSet — collection without ownership

class Listener:
    pass

listeners = weakref.WeakSet()
l1 = Listener()
l2 = Listener()
listeners.add(l1)
listeners.add(l2)

print(len(listeners))   # 2
del l1
print(len(listeners))   # 1 — l1 auto-removed

Use for observer registries, plugin systems, “things subscribing to events” — where the registry shouldn’t be responsible for keeping subscribers alive.

weakref.finalize — the modern __del__

__del__ is famously fragile (cycles, interpreter shutdown, exception swallowing). weakref.finalize is the modern replacement:

import weakref

class Resource:
    def __init__(self, name):
        self.name = name
        # Register cleanup; pass NAME (a string), NOT self
        self._finalizer = weakref.finalize(self, cleanup, name)

def cleanup(name):
    print(f"cleaning up {name}")

r = Resource("conn-1")
del r                # prints "cleaning up conn-1"

Critical: never pass self (or an attribute containing self) to the finalize callback. That creates a strong reference back to the object, preventing collection — defeating the entire mechanism. Pass primitives or weakrefs.

Why prefer weakref.finalize over __del__:

__del__ weakref.finalize
Cycles may not run if in a reference cycle always runs (deterministic)
Interpreter shutdown unreliable reliable
Exception in callback silently swallowed logged to sys.unraisablehook
Explicit args uses self.x (implicit refs everywhere) explicit args (you control captures)
Can be detached no finalizer.detach()

For libraries / production code: use weakref.finalize.

What can be weakly referenced?

Most user-defined classes work. Not these built-ins:

  • int, float, str, tuple, bool, bytes
  • list, dict, setunless you subclass them
  • frozenset — same
weakref.ref(42)             # TypeError: cannot create weak reference to 'int' object
weakref.ref([1, 2, 3])      # TypeError: cannot create weak reference to 'list' object

class WeakableList(list):
    pass

weakref.ref(WeakableList([1, 2, 3]))   # OK

Built-in containers don’t allocate a __weakref__ slot to save memory. Subclassing usually adds it. Some classes (NumPy arrays, etc.) explicitly support weakrefs.

To enable weakrefs on a class with __slots__:

class Foo:
    __slots__ = ("x", "y", "__weakref__")    # must include __weakref__

    def __init__(self, x, y):
        self.x = x
        self.y = y

Without __weakref__ in __slots__, the class can’t be weakly referenced. See 28_slots.md.

Breaking reference cycles

Parent-child structures often create cycles:

class Tree:
    def __init__(self, parent=None):
        self.parent = parent       # strong upward
        self.children = []         # strong downward

root = Tree()
leaf = Tree(parent=root)
root.children.append(leaf)
# Cycle: root → leaf → parent (root). GC handles it, but deferred.

Cycle collection runs periodically and costs CPU. Avoid it by making one direction weak:

class Tree:
    def __init__(self, parent=None):
        self._parent_ref = weakref.ref(parent) if parent else None
        self.children = []

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

Now parent → child is strong (parent keeps children alive); child → parent is weak (children don’t extend parent’s lifetime). When the parent is the last strong reference to itself, dropping it lets the whole tree collect immediately, without waiting for cycle GC.

Callback on collection

weakref.ref accepts a callback fired when the target is collected:

import weakref

class Document:
    pass

def on_collect(ref):
    print(f"document collected; weakref was {ref}")

doc = Document()
ref = weakref.ref(doc, on_collect)
del doc        # prints "document collected; weakref was <...>"

Useful for cleanup when you can’t add __del__ to the class (third-party). The callback receives the (now-dead) weakref, not the original object.

Caches: strong vs weak vs LRU

Cache type Lives until When to use
Strong (dict / lru_cache) explicitly evicted small bounded cache, fast lookup
WeakValueDictionary object collected elsewhere shared identity store; cache shouldn’t extend lifetime
functools.lru_cache(maxsize=N) LRU eviction size-bounded; memoization
cachetools.TTLCache TTL expiration time-bounded freshness

WeakValueDictionary isn’t a size-bounded cache — entries can accumulate if other parts of the code hold strong references. It’s only useful when lifetime management exists elsewhere.

Pitfalls

Capturing self in finalize

# WRONG — creates strong ref, defeats GC
class Resource:
    def __init__(self):
        weakref.finalize(self, self.cleanup)   # self.cleanup binds self

# Correct
class Resource:
    def __init__(self, name):
        weakref.finalize(self, Resource._cleanup, name)
    
    @staticmethod
    def _cleanup(name):
        print(f"cleaning {name}")

The first form ties the finalizer to a bound method that holds self. The object is never collectable.

Using weakref where strong would work

weakref is for cases where you specifically want to NOT extend lifetime. Don’t use it just because you’ve heard of it; the indirection (calling the ref, checking for None) is overhead and complexity.

WeakValueDictionary entries vanishing during iteration

for k, v in weak_value_dict.items():
    # If GC fires mid-iteration, the dict can shrink
    ...

Concurrent modification can happen due to GC. Either snapshot the keys / values first, or use weakref.WeakValueDictionary.valuerefs() and check before deref.

Forgetting weakref in slots

If a class has __slots__ and you want weak refs, include "__weakref__":

class Foo:
    __slots__ = ("x",)
    # weakref.ref(Foo()) → TypeError

Trade-off: tiny memory overhead for the weak-ref slot.

weakref to a method or function

# Bound methods can't be weakref'd directly
ref = weakref.ref(obj.method)        # TypeError or surprise

# Solution: WeakMethod
ref = weakref.WeakMethod(obj.method)
ref()                                 # bound method or None if obj collected

WeakMethod is the right tool for “subscribe a method to an event; release when the instance dies.”

Common interview confusions

  • “weakref prevents GC.” — opposite. It doesn’t prevent collection; that’s its point. Once the object is collected, the weakref returns None.
  • del obj always collects the object immediately.” — only if it was the last strong reference. Weak references don’t count toward this.
  • “All built-ins can be weakly referenced.” — many can’t (int, str, tuple, list, dict, set). Subclasses can.

Interview angle

  • “What is a weak reference and when do you use it?” — a reference that doesn’t increment the object’s refcount. The object can be collected while the weakref exists; the weakref then returns None. Used for caches, observer patterns, parent-child links — anywhere “knowing about” an object shouldn’t extend its lifetime.
  • “How would you implement a cache that doesn’t keep entries alive?”weakref.WeakValueDictionary. Entries vanish when no other part of the code holds the value. Useful for identity-mapped object stores; not a replacement for size-bounded caches like lru_cache.
  • “Why use weakref.finalize instead of __del__?”__del__ is unreliable: doesn’t run on reference cycles, can fail during interpreter shutdown, exceptions are silently swallowed. weakref.finalize is deterministic, lets you pass args explicitly (avoiding implicit captures of self), logs exceptions.
  • “What’s the trap with weakref.finalize?” — passing self (or self.method) to the callback creates a strong reference, preventing the very GC the finalizer is supposed to react to. Pass primitives or weakrefs.
  • “What types can’t be weakly referenced?” — most built-in scalars and containers: int, float, str, tuple, bool, bytes, list, dict, set, frozenset. Subclasses CAN be weakref’d. To enable on a class with __slots__, include "__weakref__" in the slots tuple.
  • “How does weakref help with reference cycles?” — parent-child structures with bidirectional strong references form cycles, which Python’s cycle GC handles but deferred and at CPU cost. Making one direction weak (weakref.ref(parent)) breaks the cycle; the structure collects immediately when the root is dropped.
  • “What’s weakref.WeakMethod?”weakref.ref(obj.method) doesn’t work because bound methods are typically temporary objects. WeakMethod weakly references the underlying instance and the method; returns the bound method or None. Used for event handlers / observers that subscribe via methods.
  • “Difference between weakref.ref and weakref.proxy?”ref is callable (ref() returns the object or None). proxy looks like the object directly (attribute access works); raises ReferenceError if the target is collected.