Blog

Python Memory Profiling: Tools and Patterns Worth Knowing

Python handles memory management automatically, so allocations stay invisible unless you explicitly look for them. For services running in production, that invisibility can become a problem. This guide walks through the layered workflow for profiling Python memory—from a single object to the entire application lifespan.

The short version

Layer Tool What It Answers
Single object sys.getsizeof() How much overhead does this container have?
One function @profile + memory_profiler Which line in this function uses the most memory?
Whole app guppy hpy().heap() Which object types dominate the heap?
Timeline mprof run + mprof plot Is memory growing over time?

Four layers of profiling, each with a job

The first layer checks a single object. Sys.getsizeof({}) returns the overhead cost of a dict, sys.getsizeof([]) the cost of a list, and sys.getsizeof(set()) the cost of a set. This is useful when choosing between container types—a list costs 32 bytes on a typical Python build, a set costs 112 bytes, and a dict costs 136 bytes. The differences are small, but they add up in code that creates millions of containers.

The second layer profiles a single function. Add the @profile decorator from the memory_profiler package to any function, then run python -m memory_profiler myscript. The output shows line-by-line memory usage as the script exits. This approach works well when you have a specific function in mind.

The third layer surveys the heap. Guppy's hpy().heap() prints a table grouped by object type. In a sample PyQt4 application, this revealed strings at 42% of heap size, tuples at 10%, and function objects at 3%—showing where to focus optimization efforts. This layer helps when you don't have a specific suspect and need to understand the application holistically.

The fourth layer traces memory over time. Running mprof run script followed by mprof plot produces a graph of memory usage across the script's lifetime. This is the only layer that reveals slow leaks, where memory grows gradually without being released. Note that mprof plot requires matplotlib.

Picking the right layer for the problem

A common mistake is jumping to the heavier tools before doing quick checks. Most container-choice issues are visible at the object level in under a minute. Skipping to guppy or mprof can mean spending hours interpreting detailed output for a problem that sys.getsizeof would have identified immediately.

Another consideration: synthetic test scripts don't always reflect production memory patterns. Where possible, profile against a realistic workload.

How to handle memory leaks

If memory leaks appear, the quick fix is allocating more memory—but this masks the problem and can create instability. A better approach is profiling to find the source. Once you identify where memory is accumulating, consider running memory-intensive tasks in separate processes. This ensures memory is released after each task completes rather than persisting across the application lifetime.

Bottom line

Start small. Use sys.getsizeof for container decisions, @profile for suspected functions, guppy when you need the full heap picture, and mprof when you need to catch slow leaks over time. Remember to remove @profile decorators before shipping code.

Frequently asked questions

How do you check the size of a single Python object?

Use sys.getsizeof({}) for a dict, sys.getsizeof([]) for a list, sys.getsizeof(set()) for a set, and similar calls for other types.

What is the easiest way to profile a single Python function?

Add the @profile decorator from the memory_profiler package to the function, then run python -m memory_profiler myscript.

How do you get a heap overview of a whole Python app?

Use guppy's hpy() and call h.heap() to print a table grouped by object type and size.

How do you see memory usage across the full lifetime of a Python script?

Run mprof run script, then mprof plot. The plotting step requires matplotlib.

How should Python memory leaks be handled?

Profile first to find the source, then consider running memory-heavy tasks in separate processes so memory is released per-task.

Related