The weakref module creates references that don't prevent garbage collection. Essential for caches, observers, and avoiding reference cycles.

The Problem

Normal references keep objects alive:

class BigObject:
    def __init__(self, name):
        self.name = name
        self.data = [0] * 1000000
 
cache = {}
 
def get_object(name):
    if name not in cache:
        cache[name] = BigObject(name)
    return cache[name]
 
# Objects stay in memory forever, even if unused
obj = get_object("big")
del obj  # Still in cache, never freed

Weak References

Weak references don't prevent garbage collection:

import weakref
 
class BigObject:
    def __init__(self, name):
        self.name = name
 
obj = BigObject("big")
weak_ref = weakref.ref(obj)
 
# Access the object
print(weak_ref())  # <BigObject object at ...>
 
# Delete the strong reference
del obj
 
# Weak ref now returns None
print(weak_ref())  # None

WeakValueDictionary

Cache that automatically removes garbage-collected values:

import weakref
 
class BigObject:
    def __init__(self, name):
        self.name = name
        self.data = [0] * 1000000
 
cache = weakref.WeakValueDictionary()
 
def get_object(name):
    obj = cache.get(name)
    if obj is None:
        obj = BigObject(name)
        cache[name] = obj
    return obj
 
# Object cached while in use
obj = get_object("big")
print("big" in cache)  # True
 
# When no strong references remain, it's removed
del obj
# cache["big"] is now gone (after GC)

WeakKeyDictionary

Store metadata about objects without keeping them alive:

import weakref
 
# Track extra data about objects
metadata = weakref.WeakKeyDictionary()
 
class User:
    def __init__(self, name):
        self.name = name
 
user = User("Alice")
metadata[user] = {"login_count": 5, "last_seen": "2024-01-01"}
 
# Access metadata
print(metadata[user]["login_count"])
 
# When user is deleted, metadata is automatically cleaned up
del user
# metadata entry is gone

WeakSet

A set that doesn't prevent garbage collection:

import weakref
 
class Observer:
    def notify(self, message):
        print(f"Received: {message}")
 
observers = weakref.WeakSet()
 
def add_observer(obs):
    observers.add(obs)
 
def notify_all(message):
    for obs in observers:
        obs.notify(message)
 
obs1 = Observer()
obs2 = Observer()
add_observer(obs1)
add_observer(obs2)
 
notify_all("Hello")  # Both receive
 
del obs1
notify_all("World")  # Only obs2 receives

Callbacks on Collection

Run code when an object is garbage collected:

import weakref
 
def cleanup(ref):
    print("Object was garbage collected")
 
obj = object()
weak_ref = weakref.ref(obj, cleanup)
 
del obj
# Prints: "Object was garbage collected"

Finalize

More robust cleanup with finalize:

import weakref
import tempfile
import os
 
class TempFileHandler:
    def __init__(self, content):
        self.path = tempfile.mktemp()
        with open(self.path, 'w') as f:
            f.write(content)
        
        # Register cleanup
        self._finalizer = weakref.finalize(
            self, 
            os.unlink, 
            self.path
        )
    
    def read(self):
        with open(self.path) as f:
            return f.read()
 
# Temp file is automatically deleted when handler is garbage collected
handler = TempFileHandler("test data")
print(handler.read())
del handler
# File is deleted

Practical Example: Object Cache with Expiry

import weakref
import time
 
class CachedObject:
    def __init__(self, key, data):
        self.key = key
        self.data = data
        self.created = time.time()
 
class SmartCache:
    def __init__(self, ttl_seconds=60):
        self._cache = weakref.WeakValueDictionary()
        self._ttl = ttl_seconds
    
    def get(self, key, factory):
        obj = self._cache.get(key)
        
        if obj is None or (time.time() - obj.created) > self._ttl:
            obj = CachedObject(key, factory())
            self._cache[key] = obj
        
        return obj.data
    
    def __len__(self):
        return len(self._cache)
 
# Usage
cache = SmartCache(ttl_seconds=30)
 
def expensive_computation():
    return {"result": 42}
 
result = cache.get("key1", expensive_computation)

When to Use Weak References

Use CaseTool
Cache valuesWeakValueDictionary
Metadata about objectsWeakKeyDictionary
Observer patternWeakSet
Cleanup on GCfinalize
Circular reference breakingweakref.ref

Limitations

Not all objects support weak references:

import weakref
 
# These work
weakref.ref(object())
weakref.ref([1, 2, 3])  # ERROR: list doesn't support weakrefs
 
# Built-in types without __weakref__ slot
# - int, str, tuple, list, dict (the types themselves)
# - But subclasses can add support

Quick Reference

import weakref
 
# Create weak reference
ref = weakref.ref(obj)
ref()  # Get object or None
 
# Callback on collection
ref = weakref.ref(obj, callback)
 
# Weak dict (values don't prevent GC)
d = weakref.WeakValueDictionary()
 
# Weak dict (keys don't prevent GC)
d = weakref.WeakKeyDictionary()
 
# Weak set
s = weakref.WeakSet()
 
# Cleanup finalizer
fin = weakref.finalize(obj, cleanup_func, *args)

Weak references solve a specific problem: holding references without ownership. When you need a cache that doesn't cause memory leaks, reach for weakref.

React to this post: