šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

Python Memory Profiling

tracemalloc and memory measurement tools that answer 'why is this process using so much memory' with a specific line number, not a guess.

⚔ Total XP: 0|šŸ’» python XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core logic.

Quick Quiz //

Why is comparing two tracemalloc snapshots (before/after a suspected operation) more useful than looking at just one snapshot?


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

A process eating unexpected amounts of memory is one of the harder production issues to diagnose by intuition — objects, references, and garbage collection interact in ways that aren't visible from reading code alone. tracemalloc, in the standard library, traces every allocation back to the exact line that made it.

1tracemalloc: Tracing Every Allocation to a Specific Line

tracemalloc, in the standard library since 3.4, hooks into Python's memory allocator and records, for every single object allocated while tracing is active, exactly which line of code triggered that allocation. tracemalloc.start() begins that tracking; tracemalloc.take_snapshot() captures the complete current state — every currently-live traced allocation, grouped and attributable back to its originating source line.

snapshot.statistics("lineno") aggregates that snapshot by source line, ranking lines by how much memory is currently attributable to allocations made there — printing the top few entries answers, with actual precision, 'which specific lines in my code are responsible for the most memory currently in use', rather than a guess based on which parts of the code *look* memory-intensive.

This line-level attribution is what makes tracemalloc fundamentally more useful for diagnosis than a single aggregate 'total memory used' number (from sys.getsizeof on some suspected culprit, or from an OS-level process monitor) — it directly answers the actual debugging question ('which line is responsible') instead of requiring you to manually narrow down the cause through trial and error.

āœ•
—
+
import tracemalloc

tracemalloc.start()

data = [str(i) * 100 for i in range(100_000)]  # allocates a lot of memory

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

for stat in top_stats[:3]:
    print(stat)  # shows the EXACT line responsible, and how much memory it used
localhost:3000
Line-Level Attribution
snapshot.statistics("lineno")[:3]
The exact lines responsible for the most currently-allocated memory

2Comparing Snapshots: Isolating What Actually Grew

A single snapshot answers 'what's using memory right now', which includes everything allocated since the program started — a lot of that is baseline overhead, imported libraries, and normal working state that isn't the actual problem. Diagnosing 'this specific operation seems to leak memory' requires isolating exactly what grew *during* that operation specifically, filtering out everything that was already present before it ran.

snapshot2.compare_to(snapshot1, "lineno") does exactly this: it computes the difference between two snapshots, line by line, showing precisely what memory was newly allocated (or freed) between the two points in time they were taken. Wrapping a suspected operation between two take_snapshot() calls and comparing them turns 'I think this function leaks memory' into a precise, ranked list of exactly which lines allocated memory during that specific operation and how much.

This before/after comparison technique is the standard, most effective tracemalloc workflow for hunting a real memory leak: rather than staring at one large, undifferentiated snapshot trying to guess what's abnormal, you isolate the suspected operation between two snapshots and let the diff show you precisely, and only, what that operation itself contributed — the memory-profiling equivalent of profiling's 'sort by cumulative time to find the real bottleneck' discipline from the previous lesson.

āœ•
—
+
snapshot1 = tracemalloc.take_snapshot()
run_suspected_leaky_operation()
snapshot2 = tracemalloc.take_snapshot()

diff = snapshot2.compare_to(snapshot1, "lineno")
for stat in diff[:5]:
    print(stat)  # shows what grew, and by how much, between the two points in time
localhost:3000
Snapshot Diff
snapshot2.compare_to(snapshot1, "lineno")
Exactly what grew during the suspected operation, ranked

3sys.getsizeof(): A Single Object's Footprint, With a Real Caveat

sys.getsizeof(obj) reports the number of bytes an individual object itself occupies in memory — useful for quick, direct comparisons, like confirming that a tuple is generally more memory-efficient than an equivalent list, or that a frozenset costs less than a set with the same elements. It's a fast, simple check for comparing the raw container overhead of different data structure choices.

The important caveat, and a common source of misleading conclusions: getsizeof() measures only the object's *own* immediate footprint, not the recursive size of everything it references. sys.getsizeof(large_list) for a list containing 1000 large strings reports only the size of the list's internal array of *pointers* to those strings — not the actual memory the 1000 string objects themselves consume, which could be dramatically larger than what getsizeof() reports for the list alone.

For a genuinely complete picture of a complex object graph's total memory footprint — a list of dictionaries, each containing nested lists, for instance — sys.getsizeof() alone is insufficient; you'd need either a recursive traversal that sums getsizeof() across every reachable object (careful to handle shared references and avoid double-counting), or a purpose-built tool like pympler or tracemalloc's own snapshot-based line-level accounting, which correctly attributes memory to where it was actually allocated regardless of how deeply nested the resulting object graph becomes.

āœ•
—
+
import sys

small_list = [1, 2, 3]
large_list = list(range(1000))

print(sys.getsizeof(small_list))   # bytes used by the list itself
print(sys.getsizeof(large_list))   # grows with the number of elements
# Note: getsizeof does NOT include the size of objects the container references
localhost:3000
Container-Only Measurement
sys.getsizeof(list_of_big_strings)
Measures the list's own pointers only — NOT the strings themselves

4Step-by-Step Breakdown

'Something is leaking memory' is one of the scariest bug reports to get without a tool that can point at the exact line responsible. tracemalloc is that tool.

tracemalloc.start() begins tracking every memory allocation -- take_snapshot() then captures the current state for analysis.

Comparing two snapshots reveals exactly what grew BETWEEN them -- the single most useful technique for finding a memory leak.

Checkpoint: Why is comparing two tracemalloc snapshots (before/after a suspected operation) more useful than looking at just one snapshot?

  • →It isolates exactly what memory was allocated specifically during that operation, filtering out everything already present before it
  • →Comparing two snapshots runs significantly faster than taking one

sys.getsizeof() gives you the size of a SINGLE object -- useful for comparing data structure choices directly.

Checkpoint: Does sys.getsizeof(large_list) include the memory used by the objects the list contains?

  • →No — it only measures the container itself, not the objects it references
  • →Yes — it includes the full recursive size of everything the list contains

With time and memory both measurable, Benchmarking gives you the rigor to compare two implementations fairly, not just intuitively.

Measure Real Memory Allocation. Finish measure_allocation(): tracemalloc reports real allocation, not a guess.

Level Up šŸš€

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported (via server-side Python execution).

FirefoxSupported

Fully supported (via server-side Python execution).

SafariSupported

Fully supported (via server-side Python execution).

EdgeSupported

Fully supported (via server-side Python execution).

Best Practices

Use snapshot comparison (before/after a suspected operation) rather than a single snapshot to hunt a leak

A single snapshot includes normal baseline memory usage that isn't the problem — comparing two snapshots isolates exactly what the suspected operation itself allocated.

Remember sys.getsizeof() does not include referenced objects' memory

For a full picture of a container's true total memory footprint, use tracemalloc's line-level accounting or a dedicated recursive-sizing tool, not a single getsizeof() call on the container alone.

Frequent Bugs

THE BUG

Concluding a data structure is memory-efficient based on sys.getsizeof() alone, without accounting for the memory used by the objects it references, leading to an incorrect comparison between two designs.

THE FIX

Use tracemalloc's snapshot-based accounting (which correctly attributes memory to allocation sites regardless of nesting) or a recursive size calculation instead of a single getsizeof() call when comparing the TRUE total memory cost of complex data structures.

Real-World Examples

Diagnosing a Memory Leak in a Long-Running Worker Process

A background worker process's memory usage grows steadily over hours of operation, and the team needs to identify which specific code path is responsible without guessing.

import tracemalloc

tracemalloc.start()
snapshot_before = tracemalloc.take_snapshot()

for _ in range(1000):
    process_one_batch()  # suspected leak source

snapshot_after = tracemalloc.take_snapshot()
diff = snapshot_after.compare_to(snapshot_before, "lineno")
for stat in diff[:5]:
    print(stat)  # pinpoints exactly which line is accumulating memory

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Drawing conclusions about a data structure's total memory cost from sys.getsizeof() on the container alone, ignoring the memory used by the objects it references.

# Misleading: only measures the list's own pointer array print(sys.getsizeof(list_of_large_objects)) # More complete: use tracemalloc to see ACTUAL total allocation tracemalloc.start() snap_before = tracemalloc.take_snapshot() build_list_of_large_objects() snap_after = tracemalloc.take_snapshot() for stat in snap_after.compare_to(snap_before, "lineno")[:5]: print(stat)

The Solution //

Use tracemalloc snapshot-based accounting, or a recursive sizing utility, for comparisons that need to account for a full, nested object graph's true memory footprint.

Lesson Glossary

[01]tracemalloc

Python's standard library memory allocation tracer, attributing allocations to specific source lines.

Code Preview
// tracemalloc context

[02]Snapshot (tracemalloc)

A captured record of all currently-traced memory allocations at a specific point in time.

Code Preview
// Snapshot (tracemalloc) context

[03]snapshot.compare_to()

A tracemalloc method computing the difference in allocations between two snapshots, isolating growth over a time window.

Code Preview
// snapshot.compare_to() context

[04]sys.getsizeof()

A function returning an object's own immediate memory footprint in bytes, excluding the size of objects it references.

Code Preview
// sys.getsizeof() context

Continue Learning