Skip to Content
Course content

329: Weak References with the weakref Module

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're managing a massive warehouse. Most of your inventory is held in place by heavy-duty steel chains (strong references). As long as a chain is attached to a crate, that crate stays in the warehouse. If you have ten different chains attached to one crate, it doesn't matter if you decide you don't need nine of them—as long as one chain remains, the crate stays. This is how Python's memory management generally works: if something points to an object, that object lives.

Now, imagine you want a directory that tells you where certain crates are, but you don't want the directory itself to be the reason a crate stays in the warehouse. Instead of a chain, you use a sticky note (a weak reference). The sticky note says, "The blue crate is in Aisle 4." If the janitor comes through and sees that every single steel chain has been removed from the blue crate, they'll throw it away—even if your sticky note is still there. When you go to check your note and head to Aisle 4, you'll simply find the crate is gone.

When Strong References Become a Burden

In Python, we usually love strong references because they prevent our data from disappearing mid-execution. But this becomes a problem when you're building things like caches or registries. If your cache holds a strong reference to every object it has ever seen, those objects can never be garbage collected, leading to a slow, agonizing memory leak.

import weakref

class LargeResource:
    def __init__(self, name):
        self.name = name
        print(f"Loading {self.name} into memory...")

    def __del__(self):
        print(f"Cleaning up {self.name}!")

# A strong reference
res = LargeResource("HeavyAsset_01")
# A weak reference
res_weak = weakref.ref(res)

print(f"Weak ref points to: {res_weak()}") # Returns the object
del res # We remove the only strong reference
print(f"Weak ref points to: {res_weak()}") # Returns None

Notice how res_weak is called like a function? That's because a weak reference isn't the object itself; it's a handle. When you call it, Python checks if the object still exists. If it does, you get the object back. If the garbage collector already reclaimed it, you get None. I've found this pattern invaluable when I need to "keep an eye" on an object without accidentally extending its lifespan.

Solving the Circular Reference Trap

One of the most annoying bugs I've encountered in large systems is the circular reference. Imagine a Parent object that owns a Child, and the Child needs a reference back to its Parent. If both use strong references, they hold each other up in a "death grip," and neither can be collected unless the garbage collector does a full sweep of unreachable cycles, which is more expensive than simple reference counting.

By making the back-reference weak, you break the cycle:

class Parent:
    def __init__(self, name):
        self.name = name
        self.child = None

class Child:
    def __init__(self, name, parent):
        self.name = name
        # Use a weakref to avoid the circular death grip
        self.parent = weakref.ref(parent)

p = Parent("Owner")
c = Child("Dependent", p)
p.child = c

# If we delete p, c's reference to p doesn't keep p alive.
del p
print(c.parent()) # Now returns None

Cleaning up Syntax with Weakref Proxies

Calling res_weak() every time is a bit clunky. If you hate the parentheses, Python provides weakref.proxy. A proxy behaves exactly like the original object—you can access attributes and call methods directly—but it doesn't increase the reference count. The catch? If the object is collected, the proxy doesn't return None; it raises a ReferenceError. It's a trade-off between cleaner code and more explicit error handling.

res = LargeResource("HeavyAsset_02")
proxy = weakref.proxy(res)

print(proxy.name) # Works exactly like a normal object
del res
# print(proxy.name) # This would raise ReferenceError

I generally prefer weakref.ref when I expect the object to disappear and want to handle it gracefully with an if statement. Use proxy when the object *should* be there for the duration of the operation, and a disappearance would actually be an exceptional error.




📋 Practical Task

Build a Memory-Efficient Asset Registry

You are building a game engine where Texture objects are expensive to load. You need a TextureRegistry that tracks all currently active textures so you don't load the same file twice. However, the registry must not prevent textures from being deleted when the rest of the game is finished with them.

Implement the following:

  • A Texture class that prints "Texture [name] created" on __init__ and "Texture [name] destroyed" on __del__.
  • A TextureRegistry class with a get_texture(name) method.
  • The get_texture method should:
    • Check if the texture is already in the registry and still alive.
    • If it exists, return it.
    • If it doesn't exist (or was garbage collected), create a new Texture, store a weak reference to it in the registry, and return the texture.

Test your implementation: Create a texture via the registry, delete your local reference to it, and verify that the registry no longer holds a strong reference (the "destroyed" message should print immediately).

Rating
0 0

There are no comments for now.

to be the first to leave a comment.