Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
Tutorial

How to Debug Python Memory Leaks Step by Step

Learn how to identify, trace, and fix Python memory leaks using built-in tools like gc, tracemalloc, and objgraph. This step-by-step guide covers common causes, practical solutions, and best practices for long-running Python applications.

July 2026 8 min read 2 views 0 hearts

You’ve probably seen it happen: your Python script starts off snappy, but after a few hours, it slows down, eats up more RAM, and eventually crashes. That’s a memory leak at work. It’s not permanent memory loss — your program simply forgets to release memory it no longer needs. Over time, unused objects pile up, and your system suffers.

Let’s walk through how to catch and fix these leaks step by step, using tools you probably already have or can install quickly.

First, let’s understand what causes the leak

Memory leaks in Python usually happen when objects hold references to each other in a way that the garbage collector can’t clean up. A common culprit is cyclic references — for example, class instances that point to each other, or callbacks that capture variables. Another big one: storing data in global lists or dictionaries that grow without bound.

The good news? Python’s built‑in tools can help you trace where the memory goes.

Step 1: Confirm you have a leak (don’t guess)

Before hunting for needles, measure memory usage over time. The simplest way is to use the gc (garbage collector) module to check object counts.

import gc

# Force a collection
gc.collect()

# Count objects by type
for obj in gc.get_objects():
    print(type(obj))

A better approach: use tracemalloc, which tracks memory allocations line by line.

import tracemalloc

tracemalloc.start()

# Your code here...

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')

for stat in top_stats[:10]:
    print(stat)

This will show you which lines of code allocate the most memory. If you see something growing each time you run a function, you’ve found a candidate.

Step 2: Find objects that should have been freed

The objgraph library (install with pip install objgraph) gives you a visual of reference chains. For example, if you suspect a list is growing forever:

import objgraph

# At a point where you think memory should be released
objgraph.show_growth(limit=10)

Watch for types that keep increasing — like list, dict, or custom objects. If you see them climbing after each operation, they aren’t being cleaned up.

Step 3: Break cyclic references manually

Sometimes you can’t avoid cycles. For example, a cache that stores results indefinitely. The fix: use weak references.

import weakref

class Cache:
    def __init__(self):
        self._store = {}

    def add(self, key, value):
        # Store a weak reference so the value can be garbage collected
        self._store[key] = weakref.ref(value)

Or better, use functools.lru_cache for simple caching — it manages memory automatically.

Step 4: Check for forgotten globals

A global list that keeps appending data is a classic leak. Instead, make sure your data structures are scoped inside functions or classes, and cleared when no longer needed.

def process_data(item):
    # Use a local variable, not a global
    temp_list = []
    temp_list.append(item)
    # When the function ends, temp_list is cleaned up

If you must keep a global, add a limit:

MAX_ITEMS = 1000
cache = []

def add_to_cache(item):
    if len(cache) >= MAX_ITEMS:
        cache.pop(0)
    cache.append(item)

Step 5: Use gc.set_debug to trace collected objects

The garbage collector can print every object it destroys. This is noisy but useful in small scripts:

import gc

gc.set_debug(gc.DEBUG_LEAK)

# Run your code
# Then force a collection
gc.collect()

You’ll see messages like gc: uncollectable <Foo object at 0x...> — those are leaks.

A real‑world example from Pythonskillset

Imagine you’re building a web scraper that fetches thousands of pages. You use a global list to store results, but you forget to clear it after each batch. After 100,000 pages, the list holds all of them in memory — boom, out of memory error.

The fix is simple: after processing each batch, set the list to None or use a new list for each run.

def scrape_pages(urls):
    results = []
    for url in urls:
        data = fetch(url)
        results.append(data)
    # process results
    return results  # No global storage

Final tips to prevent leaks

  • Avoid circular references when possible. If you need them, use weakref.
  • Always close file handles, database connections, and network sockets.
  • For long-running services, restart them periodically — both Python and your OS will appreciate it.
  • Monitor memory usage with psutil or your cloud provider’s tools.

Memory leaks can be frustrating, but with tracemalloc, gc, and a little detective work, you can often pin them down in minutes. Once you know where the memory is stuck, the fix is usually just a few lines of cleanup.

Happy debugging — and remember, Python’s garbage collector is your friend, but it can’t read your mind.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.