A normal Python reference keeps an object alive. That is usually exactly what you want: if a dictionary contains an object, the object should remain available while the dictionary needs it.

Some infrastructure has a different requirement. A cache, registry, or metadata table may want to refer to an object without becoming the reason that object stays alive forever. Python’s weakref module provides references and containers for that ownership model.

Weak references are not a general memory optimization. They are useful when object lifetime should be controlled elsewhere and an auxiliary data structure must not extend it.

Strong references imply ownership

Consider a cache backed by a normal dictionary:

class Document:
    def __init__(self, text: str) -> None:
        self.text = text


cache: dict[str, Document] = {}
doc = Document("large document contents")
cache["report"] = doc

Deleting doc does not make the Document collectible because cache still holds a strong reference to it.

That behavior is correct for an authoritative cache whose contract promises that cached entries remain available. It is less useful for an opportunistic cache that should reuse an object only while something else already needs it.

A weak reference does not keep its referent alive

weakref.ref() creates a callable reference to an object without keeping that object alive by itself:

import weakref


class Document:
    pass


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

assert ref() is doc

Calling the weak reference returns the object while it is still alive. After the referent has been collected, calling the weak reference returns None.

Code using a raw weak reference must therefore handle both outcomes.

Retrieve once before using the object

Avoid checking a weak reference and then calling it again later:

if ref() is not None:
    use(ref())

Another execution context could allow the object to disappear between those operations.

Instead, retrieve the object into a strong local reference and test that value:

obj = ref()
if obj is not None:
    use(obj)

If obj is not None, the local variable keeps the object alive for the remainder of that use.

Use WeakValueDictionary for opportunistic caches

weakref.WeakValueDictionary weakly references its values. An entry can disappear when no strong references to its value remain.

import weakref


class ParsedDocument:
    def __init__(self, source: str) -> None:
        self.source = source


cache: weakref.WeakValueDictionary[str, ParsedDocument]
cache = weakref.WeakValueDictionary()


def remember(name: str, document: ParsedDocument) -> None:
    cache[name] = document

If application code still holds a ParsedDocument, the cache can find it. If the cache becomes the only remaining holder, it does not force the document to stay alive.

This makes weak-value mappings useful for canonical-object tables and caches where recomputation is acceptable.

It also means they are inappropriate when the cache itself must guarantee retention. A weak cache can lose an entry between operations as object lifetimes change.

Use WeakKeyDictionary for attached metadata

Sometimes the object is the key rather than the value. For example, a library may want to associate diagnostic metadata with objects it does not own.

A normal dictionary would keep those key objects alive. WeakKeyDictionary removes an entry when its key is no longer strongly referenced elsewhere.

import weakref


class Connection:
    pass


labels: weakref.WeakKeyDictionary[Connection, str]
labels = weakref.WeakKeyDictionary()

connection = Connection()
labels[connection] = "primary"

This is useful when metadata should have exactly the lifetime of the object it describes.

There is an important equality edge case. Python documents that inserting a key equal to an existing key replaces the value but does not replace the original weakly referenced key. If that original key later disappears, the mapping entry is removed. Code using distinct-but-equal objects as weak keys should account for this behavior.

WeakSet tracks objects without owning them

weakref.WeakSet provides set-like membership while weakly referencing its elements.

It can be useful for tracking currently live instances, listeners, or other objects when the tracking collection must not determine their lifetime.

import weakref


class Worker:
    pass


live_workers = weakref.WeakSet()
worker = Worker()
live_workers.add(worker)

When no strong references to worker remain, it can be collected and disappears from the weak set.

Do not use a WeakSet when membership itself is supposed to keep an object registered. In that case, a normal set expresses the intended ownership more accurately.

Not every object supports weak references

Weak-reference support depends on the object’s type.

Instances of ordinary user-defined classes generally support weak references. Several built-in types do not support direct weak references. For example, a plain list cannot be passed to weakref.ref().

If a class defines __slots__, weak-reference support also needs explicit consideration. Include __weakref__ among the slots when instances must support weak references:

class Node:
    __slots__ = ("value", "__weakref__")

    def __init__(self, value: str) -> None:
        self.value = value

Without the __weakref__ slot, instances of such a slotted class do not support weak references.

Do not add weak-reference machinery speculatively. Add it when the class participates in an ownership design that needs it.

Callbacks must not resurrect ownership accidentally

A raw weak reference can receive a callback that runs when the referent is about to be finalized:

import weakref


def removed(ref: weakref.ReferenceType[object]) -> None:
    print("object removed")

The callback receives the weak reference, not the live referent. Exceptions raised by weak-reference callbacks are reported to standard error and cannot propagate normally to the code that caused collection.

Callbacks should be small and defensive. Garbage-collection timing is a poor foundation for application control flow.

If an operation requires deterministic cleanup, use an explicit method or a context manager rather than waiting for collection.

Prefer finalize for fallback cleanup

weakref.finalize() registers a function to run when an object is collected and keeps the finalizer itself alive until then.

import weakref


class Resource:
    pass


def release_handle(handle: int) -> None:
    print(f"release {handle}")


resource = Resource()
finalizer = weakref.finalize(resource, release_handle, 42)

A live finalizer invokes its callback at most once. It can also be called explicitly, which is useful for making cleanup eager while retaining finalization as a fallback.

Do not let the callback, its arguments, or its bound state retain the object being finalized. Python’s documentation specifically warns that a finalizer callback must not own a reference to its target; otherwise the target cannot become collectible in the intended way.

For resources such as files, locks, and transactions, deterministic constructs such as with remain preferable. Finalization is best treated as a safety net when explicit lifetime management is not possible.

Weak references do not define collection timing

A weak reference answers an ownership question: should this reference keep the object alive? It does not promise when an otherwise unreachable object will be destroyed.

Python implementations can differ in memory-management details, and cyclic garbage collection introduces additional timing considerations. Application correctness should therefore not depend on a weak entry disappearing at a precise moment.

This is also why tests should avoid assuming that an object vanishes immediately after one assignment or del statement unless the test deliberately controls the relevant references and collection behavior.

Weak caches trade predictability for retention behavior

A WeakValueDictionary may sound like a cache that automatically manages memory pressure, but its semantics are narrower.

An entry disappears according to object reachability, not according to cache size, age, frequency of use, or a memory budget. If callers retain all cached values, the weak cache may retain entries indefinitely. If callers retain none, entries may disappear almost immediately.

For a cache that needs a fixed maximum size or least-recently-used policy, use a cache designed around those constraints. Weak references solve accidental ownership, not general cache eviction.

Watch for hidden strong references

Weak-reference designs can be defeated by unrelated strong references.

Common sources include:

  • local variables kept longer than expected;
  • normal dictionaries or sets elsewhere in the program;
  • closures that capture the object;
  • bound methods, which retain their instance;
  • callback arguments that retain the target indirectly;
  • test fixtures and debugging tools that keep objects reachable.

When a weak entry does not disappear as expected, inspect the ownership graph before blaming the weak container.

Choose the container from the ownership rule

The simplest way to choose among the tools is to state what must not own what.

Use a normal dict or set when membership should keep objects alive.

Use WeakValueDictionary when keys may remain stable but cached values should live only while something else owns them.

Use WeakKeyDictionary when metadata should disappear with the object used as its key.

Use WeakSet when tracking an object’s presence must not extend its lifetime.

Use weakref.ref() when lower-level code needs direct observation of a referent without ownership.

Use weakref.finalize() for non-deterministic fallback cleanup, not as a replacement for explicit resource management.

Common pitfalls

Expecting a weak cache to guarantee hits

Weak entries are intentionally allowed to disappear. If a caller requires an object to remain cached, the cache needs a strong retention policy.

Assuming all Python values are weak-referenceable

Support is type-dependent. Verify the objects involved, especially built-in and slotted types.

Depending on garbage-collection timing

Correctness should not depend on exactly when a weak mapping entry or finalizer changes state.

Capturing the target in cleanup state

A finalizer that indirectly holds its own target can prevent the target from becoming unreachable. Pass only the independent information required for cleanup.

Using weak references to hide an ownership bug

If object ownership is unclear, replacing strong references with weak ones can make failures intermittent rather than fix the design. First decide which component is responsible for keeping each object alive.

Model ownership before optimizing memory

Weak references are most effective when the lifetime rule is already clear. They let caches, registries, and metadata structures observe objects without silently becoming owners.

That distinction makes them valuable in infrastructure code, but it also explains their trade-off: weakly referenced objects can disappear whenever no strong owner remains. Design callers to tolerate that fact, use explicit cleanup for resources that require deterministic release, and choose weak containers only where non-ownership is the behavior the application actually needs.