Skip to Content
Course content

360: Understanding CPython's Memory Management

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

Most of the time, we treat Python's memory management like magic. You create a list, you toss it in a function, and eventually, it just disappears. But if you've ever watched a long-running process slowly eat through your server's RAM, you know that magic has limits. To fix those leaks, you have to stop thinking about "deleting variables" and start thinking about how CPython actually tracks ownership.

The delusion of the del keyword

I see this all the time with engineers coming from C++ or Rust: they use del thinking they are manually freeing memory. They'll write something like del large_dataset and assume the RAM is instantly reclaimed. In reality, del doesn't delete an object; it deletes a reference to that object. CPython uses reference counting as its primary mechanism. Every object has a counter; when that counter hits zero, the object is immediately wiped from existence.

The problem is that del only decrements that counter by one. If that object is still sitting in a list, passed into a closure, or cached in a global dictionary, the object stays alive. I've spent hours debugging "memory leaks" that were actually just hidden references in a logging module or a forgotten global list. The naive approach is to sprinkle del everywhere and hope for the best, but that's just rearranging deck chairs on the Titanic.

When reference counting fails: The Cycle

Reference counting is fast and deterministic, but it has a fatal flaw: circular references. Imagine you're building a simple parent-child relationship for a UI tree. The parent needs a list of its children, and each child needs a reference back to its parent so it can bubble up events.

class Node:
    def __init__(self, name):
        self.name = name
        self.parent = None
        self.children = []

    def add_child(self, child):
        child.parent = self
        self.children.append(child)

If you create a parent and a child, then del both of them from your local scope, they are still pointing at each other. Their reference counts will never hit zero. This is where CPython's Generational Garbage Collector (GC) kicks in. The GC periodically pauses your program to hunt for these "islands" of unreachable objects that are only talking to each other. It's a necessary safety net, but it's expensive. The GC has to stop the world, scan objects, and figure out what's actually garbage. If you rely solely on the GC to clean up massive circular structures, you'll see unpredictable spikes in CPU usage and latency.

Breaking the cycle with weak references

The "pro" way to handle this isn't to tweak GC thresholds or call gc.collect() manually—which is usually a sign of a deeper design flaw. Instead, you should use weakref. A weak reference is a way to point to an object without increasing its reference count. It's essentially saying, "I'd like to know where this object is, but if everyone else is done with it, feel free to kill it."

In our Node example, the parent should "own" the children (strong reference), but the child should only "observe" the parent (weak reference). By using weakref.ref, we break the cycle. The moment the parent is no longer needed by the rest of the application, it can be collected immediately, and the children's weak references will simply resolve to None. This keeps the memory footprint lean and prevents the GC from having to do the heavy lifting.

It’s a trade-off. You have to handle the possibility that the object you're referencing has been collected, which adds a tiny bit of boilerplate. But in a production system, that's a price I'm always willing to pay to avoid a 2GB memory leak in a background worker.




📋 Practical Task

Fixing a Memory Leak in a Bi-Directional Cache

You have been handed a caching system where CacheEntry objects track the CacheManager that created them. Because of the circular reference, these entries are not being cleared from memory immediately, causing the application to crash during high-load stress tests.

Your Task: Modify the provided code to use the weakref module. Ensure that the CacheEntry does not keep the CacheManager alive. You must also update the get_manager method to correctly handle the fact that the reference is now weak.

import gc
import weakref

class CacheManager:
    def __init__(self):
        self.entries = []

    def create_entry(self, value):
        entry = CacheEntry(value, self)
        self.entries.append(entry)
        return entry

class CacheEntry:
    def __init__(self, value, manager):
        self.value = value
        self.manager = manager  # This is the source of the leak!

    def get_manager(self):
        return self.manager

# Test Case
def test_leak():
    manager = CacheManager()
    entry = manager.create_entry("Some Data")
    
    # We want to be able to delete the manager and have it actually disappear
    del manager
    del entry
    
    gc.collect() 
    # In the original code, these objects might persist until the next 
    # full GC collection cycle or leak entirely if __del__ is overridden.
    print("Cleanup complete.")

test_leak()
Rating
0 0

There are no comments for now.

to be the first to leave a comment.