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,byteslist,dict,set— unless you subclass themfrozenset— 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 objalways 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 likelru_cache. - “Why use
weakref.finalizeinstead of__del__?” —__del__is unreliable: doesn’t run on reference cycles, can fail during interpreter shutdown, exceptions are silently swallowed.weakref.finalizeis deterministic, lets you pass args explicitly (avoiding implicit captures of self), logs exceptions. - “What’s the trap with
weakref.finalize?” — passingself(orself.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
weakrefhelp 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.WeakMethodweakly 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.refandweakref.proxy?” —refis callable (ref()returns the object or None).proxylooks like the object directly (attribute access works); raises ReferenceError if the target is collected.