Skip to content
Python Basics: Keeping Non-owning References with weakref

Python Basics: Keeping Non-owning References with weakref

October 21, 2026

Putting an object in an ordinary dictionary keeps a strong reference to it. Sometimes a registry should find an object only while some other part of the program still owns it. The weakref module provides references and containers that do not keep their targets alive by themselves.

The examples use tiny class instances so the change in ownership is visible. No operating-system resource is opened or released. Explicit garbage-collection calls make the collection checks repeatable in this controlled demonstration.

Prepare a scratch directory

These three standalone examples were tested on Linux with Python 3.12.14. Use Bash, Python 3.12 or newer, a text editor, and the standard mktemp, rm, and rmdir utilities. No third-party packages are required. Keep assertions enabled by running without Python’s -O option.

Create a fresh directory and keep using the same terminal:

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

Save each named source file here as UTF-8. Avoid naming a source file weakref.py or gc.py. Do not treat the exact moment of automatic object collection as a portable scheduling guarantee.

1. Retrieve the target once

Save as inspect_weak_reference.py:

import gc
import weakref

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

document = Document("Draft")
reference = weakref.ref(document)
current = reference()
assert current is document
if current is not None:
    print("Title:", current.title)

del document
gc.collect()
print("Local variable keeps it alive:", reference() is current)
del current
gc.collect()
assert reference() is None
print("After last owner is gone:", reference())

Run it:

python3 inspect_weak_reference.py

Output:

Title: Draft
Local variable keeps it alive: True
After last owner is gone: None

Calling reference returns the target or None. Assign the result once and then check it; a separate check followed by a second call needlessly assumes the object remains available.

The local variable current is itself a strong reference. Removing document therefore does not release the object while current still exists. The final check succeeds only after both owning variables are removed. Deleting a variable removes a binding, not every reference to the object.

2. Index values without retaining them

Save as use_weak_value_registry.py:

import gc
import weakref

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

registry = weakref.WeakValueDictionary()
document = Document("Notes")
registry["notes"] = document
assert registry["notes"] is document
print("Keys while owned:", sorted(registry))
print("Missing key:", registry.get("missing"))

del document
gc.collect()
assert len(registry) == 0
print("Keys after collection:", sorted(registry))
print("Released value:", registry.get("notes"))

Run it:

python3 use_weak_value_registry.py

Output:

Keys while owned: ['notes']
Missing key: None
Keys after collection: []
Released value: None

The registry holds values weakly. Once the document is collected, its entry disappears. An ordinary dictionary would keep that value alive. The string key is ordinary data; the weakness applies to the value, not to both halves of the entry.

A weak registry is not durable storage or a cache with a guaranteed retention period. Callers must handle missing entries and recreate objects when appropriate. Store the result of get locally before using it, just as in the first example.

3. Run a finalizer at most once

Save as call_object_finalizer.py:

import gc
import weakref

class Session:
    pass

events = []
session = Session()
finish = weakref.finalize(session, events.append, "finished")
finish.atexit = False
print("Initially pending:", finish.alive)
finish()
finish()
del session
gc.collect()

assert events == ["finished"]
assert not finish.alive
print("Events:", events)
print("Still pending:", finish.alive)

Run it:

python3 call_object_finalizer.py

Output:

Initially pending: True
Events: ['finished']
Still pending: False

The finalizer records one event when called explicitly. Calling it again and later collecting the target do not repeat the callback. Setting atexit to False excludes interpreter-exit behavior from this small example.

The callback must not retain the target, directly or indirectly. Passing a bound method of session could keep session alive and defeat automatic finalization. Here the callback belongs to the separate events list, not the Session object.

Use a context manager or explicit close for timely resource management; a finalizer is not a substitute for predictable cleanup. Not every object supports weak references: plain lists and dictionaries do not. The ordinary custom classes used here do support them; classes using slots need additional care.

Clean up

In the same terminal, remove only these source files, return to the original directory, and remove the now-empty scratch directory:

rm -- inspect_weak_reference.py use_weak_value_registry.py call_object_finalizer.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

If rmdir reports that anything remains, inspect the directory rather than deleting unknown files.

References

Acknowledgements

This post was created with assistance from AI.

Last updated on