Most Python code should use ordinary references. If one object stores another object in an attribute, list, or dictionary, that reference normally means the stored object should remain available for as long as the owner needs it.

Some relationships are different. A cache may want to reuse an object only while another part of the program already owns it. A registry may want to discover live objects without extending their lifetime. An observer table may want to remember listeners without becoming the reason those listeners can never be collected.

These are non-owning relationships. Python’s weakref module exists for them.

The useful mental model is simple:

strong reference -> contributes to keeping an object alive
weak reference   -> can observe an object while it is alive

A weak reference is not a replacement for normal ownership. It is a specialized tool for cases where the relationship should disappear naturally when the target object is no longer strongly referenced elsewhere.

Start with the ownership problem

Suppose an application keeps recently constructed document objects in a normal dictionary:

cache = {}


def remember(document):
    cache[document.id] = document

The dictionary now owns strong references to every stored document. Even if the rest of the application stops using one, the cache still keeps it alive.

That may be exactly what you want for a bounded cache with an explicit eviction policy. But it is wrong if the intended rule is:

Reuse the object when somebody else still needs it, but do not keep it alive only because it appeared in this registry.

A weak reference expresses that second rule.

A weak reference does not keep its target alive

The low-level API is weakref.ref:

import weakref


class Document:
    pass


document = Document()
reference = weakref.ref(document)

assert reference() is document

Calling the weak-reference object returns the target while the target is still alive.

After the last strong reference disappears, the weak reference eventually stops resolving to the object:

del document

if reference() is None:
    print("document is no longer available")

The important point is not that reference() behaves like a special nullable pointer. The important point is ownership: reference itself is not enough to keep the Document instance alive.

Do not separate the liveness check from retrieval

It is tempting to write:

if reference() is not None:
    reference().process()

That calls the weak reference twice. In concurrent code, the object could disappear between those calls.

Retrieve it once instead:

document = reference()

if document is not None:
    document.process()

The local variable document is now a normal strong reference for the duration of that use, so the object cannot disappear halfway through the operation merely because the weak reference changed state.

Prefer weak containers when the relationship is a collection

Application code often does not need to manage individual weakref.ref objects. The standard library provides collection types that remove entries automatically when their weakly referenced objects disappear.

The most common are:

  • weakref.WeakValueDictionary, which holds values weakly;
  • weakref.WeakKeyDictionary, which holds keys weakly;
  • weakref.WeakSet, which holds its elements weakly.

These types are usually clearer than building a custom dictionary of raw weak references.

Use WeakValueDictionary for a non-owning registry

A WeakValueDictionary behaves like a mapping whose values do not stay alive solely because the mapping contains them.

import weakref


class Document:
    def __init__(self, document_id):
        self.document_id = document_id


registry = weakref.WeakValueDictionary()

document = Document("doc-42")
registry[document.document_id] = document

assert registry["doc-42"] is document

As long as document remains strongly referenced, the registry can find it.

When the last strong reference disappears, the corresponding mapping entry can disappear as well:

del document

assert "doc-42" not in registry

This pattern is useful for identity registries and some caches where keeping an object alive is not part of the cache’s responsibility.

It is not a general replacement for caching. If the cache is supposed to retain expensive results for a defined period, a weak-value cache may discard them sooner than the application expects.

Weak caches trade retention guarantees for lower ownership pressure

A normal cache answers this question:

Should the cache keep this value available for later reuse?

A weak cache answers a different question:

If the value is still alive somewhere else, can we reuse that same object?

Those policies produce different behavior.

Imagine an expensive parsed schema:

normal cache
request ends -> cache still owns schema -> schema remains reusable

weak cache
request ends -> no other strong owner -> schema may disappear

A weak cache therefore does not guarantee a hit merely because a key was inserted previously. If deterministic retention matters, use a normal cache with a size, lifetime, or eviction policy you can reason about.

Weak references are best when opportunistic reuse is enough.

Use WeakKeyDictionary for metadata attached externally

Sometimes you want to associate metadata with an object that another part of the program owns.

For example, a library may want to remember when objects were inspected without modifying those objects directly:

import weakref


last_seen = weakref.WeakKeyDictionary()


def mark_seen(obj, timestamp):
    last_seen[obj] = timestamp

The metadata entry disappears when the key object is no longer strongly referenced elsewhere.

That can be useful for:

  • auxiliary metadata managed outside the object;
  • memoized information tied to object identity;
  • instrumentation that should not extend application object lifetimes.

Equal objects can make WeakKeyDictionary surprising

WeakKeyDictionary is not simply an identity-only table.

If two distinct key objects compare equal and have compatible hashes, inserting the second can replace the value associated with the first key without necessarily replacing the stored key object itself. If the original key later disappears, the entry may disappear too.

That behavior matters for value-like objects whose equality is broader than identity.

When metadata must be tied strictly to a particular object identity, verify that the key type’s equality semantics match the design. Do not assume weak-key storage automatically means identity semantics in every detail.

WeakSet is useful for non-owning membership

A WeakSet is appropriate when you only need to know which live objects belong to a group.

import weakref


class Listener:
    pass


listeners = weakref.WeakSet()
listener = Listener()
listeners.add(listener)

assert listener in listeners

If nothing else owns listener, the set does not keep it alive.

This can fit observer-style relationships where the publisher should not own the listener’s lifetime.

However, event systems have more concerns than lifetime alone. You still need to decide how callbacks are invoked, how failures are isolated, whether ordering matters, and whether bound methods are stored correctly.

Bound methods need special care

A bound method such as service.handle is a temporary object created from two pieces: the instance and the underlying function.

A plain weak reference to a bound method is often not useful because that temporary bound-method object may disappear immediately even while the instance itself is still alive.

Python provides weakref.WeakMethod for this case:

import weakref


class Service:
    def handle(self):
        return "handled"


service = Service()
callback = weakref.WeakMethod(service.handle)

method = callback()
assert method is not None
assert method() == "handled"

WeakMethod can recreate the bound method while both the instance and original function still exist.

This is useful when implementing callback registries that should not own subscriber instances.

Not every Python object supports weak references

Weak referencing is a capability of the target object’s type.

Instances of ordinary user-defined classes generally support weak references. Many built-in and extension types support them too, but support is not universal.

For example, a plain list cannot be weakly referenced directly:

import weakref


values = []
weakref.ref(values)  # raises TypeError

Do not infer weak-reference support from whether an object is mutable, hashable, or otherwise container-like. Those are separate properties.

If your design accepts arbitrary user objects, be prepared for TypeError when weak-reference support is not guaranteed by the interface.

slots can disable weak-reference support

A user-defined class normally gets weak-reference support automatically. Defining __slots__ changes that unless the class hierarchy still provides a weak-reference slot.

This class does not support weak references:

class Point:
    __slots__ = ("x", "y")

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

If instances need to be weakly referenceable, include __weakref__:

class Point:
    __slots__ = ("x", "y", "__weakref__")

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

This is easy to miss when a class is later optimized with slots and code elsewhere already depends on weak references.

Treat weak-reference support as part of the class’s behavioral contract when other components rely on it.

Use finalize when cleanup should follow object lifetime

weakref.finalize registers a callback that can run when an object is collected without requiring the class to implement __del__.

A small example:

import weakref


class ResourceOwner:
    pass


owner = ResourceOwner()
finalizer = weakref.finalize(owner, print, "owner released")

assert finalizer.alive

A finalizer runs at most once. It can also be invoked explicitly, which marks it as no longer alive.

This can be useful as a fallback cleanup mechanism, especially when you do not control the target class.

Do not make the finalizer keep the target alive

The cleanup function and its arguments must not strongly reference the target object, directly or indirectly.

This is risky:

class ResourceOwner:
    def close(self):
        pass


owner = ResourceOwner()
weakref.finalize(owner, owner.close)

The bound method owner.close contains a strong reference to owner. The finalizer now helps keep alive the very object whose disappearance is supposed to trigger it.

Pass only the independent cleanup state that is required:

weakref.finalize(owner, close_resource, resource_id)

The cleanup callback can then release the external resource without owning the target object.

Finalizers are a fallback, not deterministic resource management

Garbage collection is not a substitute for explicit lifetime boundaries.

If a file, transaction, lock, socket, or temporary directory must be released at a predictable point, prefer deterministic mechanisms such as:

  • a with statement and context manager;
  • an explicit close() or equivalent lifecycle method;
  • try/finally around ownership that cannot use a context manager.

A finalizer is more appropriate as a safety net for cleanup that may otherwise be missed.

The distinction matters because object collection timing is not an application-level scheduling guarantee. Code should not depend on a finalizer running immediately after the last obvious use of an object.

Weak references do not solve reference cycles by themselves

Weak references can prevent a particular relationship from becoming a strong ownership edge, which can help avoid cycles in some designs.

But they are not a general instruction to replace one side of every object graph with weak references.

Python’s garbage collector can detect many unreachable reference cycles. More importantly, the correct design question is still ownership:

Which object is responsible for keeping which other object alive?

Use a weak reference when the answer is this relationship should not own the target. Do not use one merely because two objects reference each other.

Avoid depending on exact collection timing

Examples often use del and then immediately show a weak mapping becoming empty. That is convenient for explanation, but application logic should not require one exact collection schedule across Python implementations.

del name removes a reference from that name. It does not itself promise that the target object has been destroyed at that exact line.

For this reason, avoid designs where correctness depends on a weak-container entry disappearing immediately. The container should be treated as a view of objects that are currently reachable strongly elsewhere, not as a deterministic lifecycle notification system.

If you need a reliable state transition, represent that transition explicitly in application logic.

Common mistakes

Using weak references when ownership is actually required

If an object must remain available until a job completes, configuration reloads, or a cache entry expires, keep a strong reference for that required lifetime.

Treating a weak cache as a normal cache with automatic memory management

Weak values may disappear whenever no other strong references remain. That policy can cause recomputation and unpredictable hit rates if you expected the cache itself to retain entries.

Assuming every object can be weakly referenced

Support depends on the target type. Validate the contract or handle TypeError when arbitrary objects are accepted.

Storing a bound method in a finalizer for its own instance

That bound method strongly references the instance and can prevent the desired lifecycle behavior.

Calling a weak reference repeatedly during one operation

Resolve it once, check for None, and hold the returned strong reference while using the object.

When weak references are the right tool

Weak references are a good fit when all of the following are true:

  1. another part of the program is the real owner of the object;
  2. your component only wants to observe, index, annotate, or opportunistically reuse that object;
  3. your component should naturally forget the relationship when the real owners disappear;
  4. losing the weakly held entry is acceptable behavior rather than a correctness failure.

If any of those conditions is false, a normal strong reference is usually clearer.

Keep ownership explicit

Weak references are easiest to use correctly when they are viewed as an ownership tool rather than a memory trick.

Use strong references for objects your component is responsible for keeping alive. Use WeakValueDictionary, WeakKeyDictionary, WeakSet, or WeakMethod when a relationship should exist only while another owner keeps the target alive. Use finalize as a carefully designed fallback for lifecycle cleanup, not as a replacement for deterministic resource management.

That distinction keeps caches, registries, observers, and metadata tables from silently becoming object owners. More importantly, it makes lifetime behavior understandable from the design instead of leaving it to accidental reference chains.