Skip to content
Codeloom
Python

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.

·8 min read · By Codeloom
Advanced 13 min read

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.