Python Weak References and Garbage Collection
Understand Python's garbage collection, reference counting, and how weak references prevent memory leaks in caches, observers, and circular structures.
What you'll learn
- ✓How Python reference counting and garbage collection work
- ✓Using weakref to avoid memory leaks
- ✓Building weak-referenced caches and observer patterns
- ✓Debugging reference cycles with gc module
Prerequisites
- •Solid understanding of Python classes
- •Familiarity with Python memory model
- •Basic knowledge of data structures
How Python Manages Memory
Python uses two mechanisms to reclaim memory: reference counting and a cyclic garbage collector.
Every Python object has a reference count. When you assign an object to a variable, pass it to a function, or store it in a container, the reference count increases. When a variable goes out of scope or is reassigned, the count decreases. When it reaches zero, the memory is freed immediately.
import sys
a = [1, 2, 3]
print(sys.getrefcount(a)) # 2 (a + the temporary reference from getrefcount)
b = a
print(sys.getrefcount(a)) # 3
del b
print(sys.getrefcount(a)) # 2
Reference counting is fast and deterministic, but it cannot handle circular references, where two or more objects reference each other. That is where the cyclic garbage collector steps in.
Circular References and the GC
import gc
class Node:
def __init__(self, name):
self.name = name
self.partner = None
def __del__(self):
print(f"Deleting {self.name}")
# Create a cycle
a = Node("A")
b = Node("B")
a.partner = b
b.partner = a
# Delete the names, but the cycle keeps both alive
del a
del b
# Neither __del__ is called yet because of the cycle
# Force garbage collection
gc.collect()
# Now both are collected and __del__ is called
The gc module detects and breaks these cycles periodically. You can inspect and control it:
import gc
# Check GC thresholds (triggers collection at these counts)
print(gc.get_threshold()) # (700, 10, 10)
# Get GC statistics
print(gc.get_stats())
# Disable automatic GC (rarely needed)
gc.disable()
# Manually trigger collection
collected = gc.collect()
print(f"Collected {collected} objects")
gc.enable()
What Are Weak References?
A weak reference is a reference that does not increase the reference count. This means the referenced object can be garbage collected even while weak references to it exist. When the object is collected, the weak reference returns None (or triggers a callback).
import weakref
class Expensive:
def __init__(self, data):
self.data = data
def __repr__(self):
return f"Expensive({self.data!r})"
obj = Expensive("important data")
# Create a weak reference
weak = weakref.ref(obj)
# Access the object through the weak reference
print(weak()) # Expensive('important data')
# Delete the strong reference
del obj
# The weak reference now returns None
print(weak()) # None
Weak Reference Callbacks
You can register a callback that fires when the referenced object is collected:
import weakref
class Resource:
def __init__(self, name):
self.name = name
def on_collected(ref):
print(f"Resource was garbage collected, ref: {ref}")
resource = Resource("database-connection")
weak = weakref.ref(resource, on_collected)
print(weak()) # <Resource object>
del resource
# Output: Resource was garbage collected, ref: <weakref at 0x...; dead>
WeakValueDictionary: Building Caches
The most common use case for weak references is caching. A WeakValueDictionary holds weak references to its values, so cached objects are automatically evicted when no other code holds a strong reference:
import weakref
class Image:
def __init__(self, filename):
self.filename = filename
self.pixels = bytearray(1024 * 1024) # Simulate 1MB of data
print(f"Loaded {filename}")
def __repr__(self):
return f"Image({self.filename!r})"
# Cache that doesn't prevent garbage collection
cache = weakref.WeakValueDictionary()
def load_image(filename):
image = cache.get(filename)
if image is not None:
print(f"Cache hit: {filename}")
return image
image = Image(filename)
cache[filename] = image
return image
# First load: creates the object
img1 = load_image("photo.png") # Loaded photo.png
img2 = load_image("photo.png") # Cache hit: photo.png
print(img1 is img2) # True
# Delete all strong references
del img1, img2
# The cache entry is automatically removed
print(dict(cache)) # {}
Compare this with a regular dictionary cache that would hold objects in memory forever. WeakValueDictionary lets the garbage collector do the cache eviction for you.
WeakKeyDictionary: Attaching Metadata
WeakKeyDictionary uses weak references for keys. This is useful for attaching extra data to objects without preventing their collection:
import weakref
# Store metadata about objects without preventing GC
metadata = weakref.WeakKeyDictionary()
class Connection:
def __init__(self, host):
self.host = host
conn = Connection("db.example.com")
metadata[conn] = {
"created_at": "2026-07-06",
"pool": "primary",
}
print(metadata[conn]) # {'created_at': '2026-07-06', 'pool': 'primary'}
# When the connection is collected, the metadata entry disappears
del conn
print(len(metadata)) # 0
WeakSet: Tracking Instances
WeakSet is perfect for tracking all living instances of a class:
import weakref
class Plugin:
_instances = weakref.WeakSet()
def __init__(self, name):
self.name = name
Plugin._instances.add(self)
@classmethod
def active_plugins(cls):
return [p.name for p in cls._instances]
p1 = Plugin("auth")
p2 = Plugin("logging")
p3 = Plugin("cache")
print(Plugin.active_plugins()) # ['auth', 'logging', 'cache']
del p2
print(Plugin.active_plugins()) # ['auth', 'cache']
Without WeakSet, the class-level set would keep every instance alive forever, causing a memory leak.
Observer Pattern with Weak References
The observer pattern often causes memory leaks because the subject holds strong references to observers, preventing them from being garbage collected. Weak references solve this:
import weakref
class EventEmitter:
def __init__(self):
self._listeners = {}
def on(self, event, callback):
if event not in self._listeners:
self._listeners[event] = []
# Store a weak reference to the bound method's object
if hasattr(callback, '__self__'):
ref = weakref.WeakMethod(callback, self._make_cleanup(event))
else:
ref = weakref.ref(callback, self._make_cleanup(event))
self._listeners[event].append(ref)
def _make_cleanup(self, event):
def cleanup(ref):
if event in self._listeners:
self._listeners[event] = [
r for r in self._listeners[event] if r() is not None
]
return cleanup
def emit(self, event, *args, **kwargs):
if event not in self._listeners:
return
for ref in self._listeners[event]:
callback = ref()
if callback is not None:
callback(*args, **kwargs)
class Logger:
def __init__(self, name):
self.name = name
def on_data(self, data):
print(f"[{self.name}] Received: {data}")
emitter = EventEmitter()
logger = Logger("app")
emitter.on("data", logger.on_data)
emitter.emit("data", "hello") # [app] Received: hello
del logger
emitter.emit("data", "world") # Nothing happens, logger was collected
Notice the use of weakref.WeakMethod, which is necessary for bound methods because a regular weakref.ref to a bound method would be collected immediately (bound methods are created on the fly).
Finalize: Guaranteed Cleanup
weakref.finalize registers a cleanup function that runs when an object is garbage collected, even if the program exits normally:
import weakref
import tempfile
import os
class TempFileManager:
def __init__(self):
self.path = tempfile.mktemp()
with open(self.path, 'w') as f:
f.write("temporary data")
# Register cleanup that runs when this object is collected
self._finalizer = weakref.finalize(self, self._cleanup, self.path)
@staticmethod
def _cleanup(path):
if os.path.exists(path):
os.remove(path)
print(f"Cleaned up {path}")
def __repr__(self):
return f"TempFileManager({self.path!r})"
manager = TempFileManager()
print(manager)
# Cleanup happens automatically when the object is collected
del manager
# Output: Cleaned up /tmp/...
Unlike __del__, finalize is reliable and does not suffer from issues with interpreter shutdown ordering.
Debugging Reference Cycles
The gc module provides tools for finding reference cycles:
import gc
class Leaky:
def __init__(self, name):
self.name = name
self.ref = None
# Create objects with a cycle
a = Leaky("A")
b = Leaky("B")
a.ref = b
b.ref = a
del a, b
# Enable debug output
gc.set_debug(gc.DEBUG_SAVEALL)
# Collect and inspect what was found
gc.collect()
print(f"Garbage objects: {len(gc.garbage)}")
for obj in gc.garbage:
if isinstance(obj, Leaky):
print(f" Found leaked {obj.name}")
# Clean up
gc.garbage.clear()
gc.set_debug(0)
Using objgraph for Visualization
The third-party objgraph library helps visualize reference chains:
# pip install objgraph
import objgraph
class Container:
def __init__(self):
self.items = []
c = Container()
c.items.append(c) # Circular reference
# Find objects with the most references
objgraph.show_most_common_types(limit=5)
# Count instances of a type
print(objgraph.count("Container"))
# Find reference chains to a specific object
objgraph.show_backrefs(c, max_depth=3, filename="refs.png")
When to Use Weak References
Use weak references when:
- Caching: Objects should be evictable when memory is needed.
- Observer/listener patterns: Subscribers should not be kept alive just because they are registered.
- Instance tracking: You want to know what instances exist without preventing cleanup.
- Circular reference avoidance: Break cycles by making one direction weak.
Do not use weak references for:
- Small, short-lived objects where the overhead is not worth it.
- Built-in types like
int,str,list,dict(most do not support weak references). - Objects where you need guaranteed access (the referent might disappear).
Objects That Support Weak References
Not all Python objects support weak references. By default, instances of user-defined classes do. Built-in types like int, str, tuple, and list do not. You can enable weak reference support in C extensions by adding tp_weaklistoffset.
import weakref
# This works
class MyClass:
pass
weakref.ref(MyClass())
# This fails
try:
weakref.ref(42)
except TypeError as e:
print(e) # cannot create weak reference to 'int' object
# Subclasses of built-ins can add support via __slots__
class WeakableList(list):
__slots__ = ('__weakref__',)
weakref.ref(WeakableList([1, 2, 3])) # Works
Wrapping Up
Python’s memory management combines reference counting for fast, deterministic cleanup with a cyclic garbage collector for handling reference cycles. Weak references give you fine-grained control over this system, letting you build caches that self-evict, observer patterns that avoid leaks, and instance trackers that do not interfere with garbage collection. The weakref module provides ref, WeakValueDictionary, WeakKeyDictionary, WeakSet, and finalize as the primary tools. Combined with the gc module for debugging, you have everything needed to write memory-efficient Python programs that clean up after themselves.
Related articles
- JavaScript FinalizationRegistry and WeakRef in JavaScript
Learn how to use WeakRef and FinalizationRegistry for custom cleanup callbacks, weak caching, and observing garbage collection in JavaScript.
- Python Python Memory Management and Garbage Collection Internals
Explore how CPython manages memory with reference counting, generational garbage collection, and practical tips to avoid memory leaks.
- Pandas Pandas Categorical Data Tutorial
Use Pandas Categorical dtype to cut memory, speed up groupby, and encode ordered categories cleanly with practical conversion and pitfall notes.
- Python Python Concurrency: asyncio vs threading vs multiprocessing
Compare Python's concurrency models side by side. Learn when to use asyncio, threading, or multiprocessing with practical benchmarks and real-world examples.