AlgoMaster Logo

Weak References

Low Priority18 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

Python keeps an object alive as long as anything refers to it. That's usually what you want, but sometimes a reference should be more like a bookmark: "if the object is still around, give it to me; if not, that's fine." The weakref module provides exactly that, and it's what caches, observer registries, and parent-pointers reach for when they don't want to accidentally pin objects in memory.

Strong References vs Weak References

Every normal name in Python is a strong reference. As long as some strong reference exists, the object can't be garbage collected. That's why this works:

Even though we deleted headphones, the Product object is still alive because also_headphones is a strong reference to the same object.

A weak reference is different. It points at an object, but doesn't keep it alive. The moment the last strong reference goes away, the object is collected, and any weak reference to it quietly turns into None.

weakref.ref(obj) returns a callable. Calling it with no arguments either gives you the original object or returns None if the object has been collected. That's the whole interface.

Here's the relationship visually. The dashed arrow is the weak reference: it knows about the object but doesn't count toward keeping it alive.

The strong reference headphones is what keeps the Product alive. The weak reference ref only watches. When the strong reference disappears, the object can be collected and the weak reference becomes a dead pointer that returns None.

Which Objects Support Weak References

Not every Python object can have a weak reference taken to it. The object's type has to opt in. Most built-in containers and immutable types don't.

The reason is implementation-driven. Weak references need a small extra slot on the object to hold the list of weak references that point at it. Built-in containers like list, dict, tuple, int, and str are tuned for size, so they skip that slot. User-defined classes get it by default.

TypeSupports weak refs?
User-defined classYes (by default)
list, dict, set, tupleNo
int, float, str, bytesNo
Subclass of list or dictYes
Slotted class without __weakref__No

If you really need a weak reference to a built-in, subclass it. A trivial wrapper class works:

The wrapper inherits everything list does, but as a user-defined class it gets the weak-reference slot for free.

Re-enabling Weak Refs on a Slotted Class

A class that uses __slots__ loses the weak-reference slot unless you put it back explicitly. The fix is to include '__weakref__' in the slots tuple.

Without '__weakref__' in __slots__, the same weakref.ref(p) would raise TypeError: cannot create weak reference to 'Product' object.

weakref.proxy Instead of weakref.ref

weakref.ref is callable. To use the object, you have to call the reference and remember to handle the None case. That's fine for some patterns, awkward for others.

weakref.proxy(obj) returns a transparent stand-in. Attribute access, method calls, and operators go straight through to the underlying object, with no () needed.

The proxy looks and acts like the original object. The difference shows up when the original is gone.

A dead ref returns None. A dead proxy raises ReferenceError the moment you touch it. That's the trade-off:

ToolAccessWhen the target diesBest for
weakref.refCallable: ref()Returns NoneExplicit "is it still there?" checks
weakref.proxyTransparent attribute useRaises ReferenceError on accessPassing into code that expects the object directly

Use ref when you want to test for liveness before doing work. Use proxy when the calling code shouldn't know it's holding a weak reference at all (a child holding a reference back to its parent, for example).

WeakValueDictionary: A Cache That Doesn't Pin Memory

A normal dict keeps both its keys and its values alive. That makes it a terrible cache: every product you cache is a product you can never collect.

weakref.WeakValueDictionary solves this. Keys are held normally, but values are held weakly. When the last strong reference to a value goes away, the entry quietly drops out of the dictionary.

Once cable is deleted, the Product(202) object has no strong references left, so it's collected and the cache automatically loses entry 202. The headphones product stays because the variable still holds it.

This is exactly what you want from a "lookup by id" cache. The application owns its live products through normal variables. The cache exists so that two pieces of code asking for product 101 at the same time can share the same in-memory object, without the cache itself becoming a memory leak.

The application's strong reference is what keeps the product alive. The cache only watches. When nothing else holds product 202, the entry disappears on its own. No manual eviction code needed.

A subtle catch: values you assign must support weak references. Storing a plain dict or list as a value in WeakValueDictionary raises TypeError. The values usually need to be instances of your own classes.

WeakKeyDictionary: Attaching Data to Objects You Don't Own

WeakKeyDictionary is the mirror image. Keys are held weakly, values normally. When a key object is collected, the entry disappears.

This is the right tool when you want to attach side-data to objects without modifying their class. Imagine you want to track how many times each customer object has viewed a product, but you don't want to add a view_count attribute to Customer.

When priya is dropped from the application, the entry for it vanishes from view_counts. No leaks, no cleanup function, no __del__ method.

Compared to a plain dict, the key difference is who owns the lifetime:

ContainerKeys heldValues heldUse case
dictStrongStrongNormal storage
WeakValueDictionaryStrongWeakLookup-by-id caches
WeakKeyDictionaryWeakStrongSide-data attached to external objects
WeakSetWeak(not used)Registries that don't pin members

WeakSet: A Registry Without the Leak

Observer patterns are the classic place this matters. A Cart wants to notify interested parties when an item is added. If the cart holds those observers in a plain set, the cart will keep them alive forever, even after the rest of the application has dropped them. That's how observer-pattern memory leaks happen.

weakref.WeakSet fixes it. Members are held weakly; if a member is collected anywhere else, it disappears from the set.

After del recommender, nothing else holds the RecommendationEngine, so the WeakSet automatically drops it. The cart's notification loop only sees the observers that are still in use elsewhere.

Callbacks and weakref.finalize

Sometimes you don't just want to observe whether an object is alive, you want to run code the moment it's collected. weakref.ref accepts a callback that fires when the target dies:

The callback receives the dead ref as its only argument. The original object is already gone by then, so trying to dereference it inside the callback gets you None.

For richer cleanup, weakref.finalize is usually a better fit. It binds a callable to an object's lifetime, with arbitrary positional and keyword arguments. When the object is collected, the callable runs.

finalize has a few advantages over ref callbacks. The callable can take the arguments you want, you can check .alive, you can disable the finalizer explicitly with .detach(), and it doesn't trip over the awkward "the object is already gone when the callback runs" problem.

A common use is releasing a resource that an object owns: a temp file, a network handle, a lock. __del__ works for this too, but __del__ doesn't run reliably during interpreter shutdown and can interact badly with reference cycles. finalize is the modern recommendation for cleanup tied to object lifetime.

The lifecycle is straightforward: register the finalizer when the object is created, the callback runs exactly once when the object is collected, and the finalizer reports alive = False afterwards. You can also call finalizer() directly to run the callback early and detach it.

Common Pitfalls

A few traps catch people the first time they reach for weakref.

Weak References to Bound Methods Die Immediately

You might be tempted to register a method as an observer callback:

tracker.on_item_added is a bound method created on demand. The expression returns a fresh method object every time it's evaluated, and that fresh object has no other strong references. By the time weakref.ref returns, the bound method is already eligible for collection.

The fix is WeakMethod, which handles bound methods correctly by holding the underlying function and a weak reference to self:

WeakMethod is the right tool for any registry that wants to hold method callbacks without keeping the instance alive.

Iteration Can Race the Garbage Collector

All four weak containers (WeakValueDictionary, WeakKeyDictionary, WeakSet, and any structure built on weakref.ref callbacks) can drop entries between two adjacent operations. If you iterate without snapshotting, you can hit surprising results.

Once products is set to None and the GC runs, every value in the cache is collected and every entry drops out. If you happened to be iterating cache.items() when this happened, your loop would see entries vanishing mid-iteration. Snapshot with list(...) when you need a stable view across operations.

Don't Use a Weak Reference for the Last Strong Reference

The point of a weak reference is that something else owns the object. If only a weak reference exists, the object will be collected immediately, and the weak reference will be useless:

The Product("headphones") expression creates an object with no name attached. By the time weakref.ref(...) returns, no strong reference exists, so the object is already eligible for collection. The fix is obvious in retrospect: keep a strong reference somewhere before taking the weak one.

Weak References Help Avoid Cycles

A bidirectional parent-child relationship is the classic place this matters. If a Cart holds its items strongly and each CartItem holds a strong reference back to its cart, you've made a cycle. Python's cycle collector will eventually clean it up, but cycle collection runs less often than reference counting and can complicate finalization.

The convention is to make the child's reference to its parent a weak reference (or a proxy), so the relationship is asymmetric.

The cart owns the items strongly. The items only observe the cart weakly. No cycle, no extra cleanup work. The takeaway is that weak references let you avoid the cycle in the first place.

Putting It Together: A Shared Product Cache

A small, realistic example. The application loads products on demand, but if two callers ask for the same product id at the same time, they should share the same in-memory object. The cache shouldn't keep products alive on its own.

While the application holds the product, the cache returns the same object on every lookup. Once nothing else references it, the cache entry quietly disappears, and the next lookup creates a fresh instance from the underlying data. That's the exact behaviour you want from an identity-preserving cache.

Quiz

Weak References Quiz

10 quizzes