skip to content

How do you use tracemalloc to find which lines of a script allocated the most memory?

level: juniorimportance: should knowfreq 25%

answer

  1. Turn it on before the work runs
  2. Three calls, then read the list
  3. A frozen snapshot you can query
  4. Group by file and line number
  5. Statistic gives size and block count

basics

~10 s

Call tracemalloc.start() before the work, tracemalloc.take_snapshot() after it, then sort the snapshot with Snapshot.statistics('lineno'). Each entry names a file and line and reports the bytes and block count still allocated there.

solid answer

~30 s

`tracemalloc` is the standard library's allocation tracer. You call `tracemalloc.start()` to begin recording, run the code you suspect, then `tracemalloc.take_snapshot()` to capture the blocks that are alive at that instant. `Snapshot.statistics('lineno')` returns a list of `Statistic` objects sorted largest-first, each with `size` in bytes, `count` (number of allocated blocks) and a `traceback` you can print with `Traceback.format()`. Two limits matter: only allocations made **after** `start()` are recorded, and only allocations that go through CPython's own memory allocators are seen. `tracemalloc.get_traced_memory()` gives a `(current, peak)` byte pair if you just want totals, and `tracemalloc.stop()` turns tracing off and discards the traces.

code

python · 10 lines
python
import tracemalloc

tracemalloc.start()
rows = [{"id": i, "title": f"item {i}"} for i in range(50_000)]
snapshot = tracemalloc.take_snapshot()
for stat in snapshot.statistics("lineno")[:3]:
    print(f"{stat.size / 1024:.1f} KiB in {stat.count} blocks")
    print("   ", stat.traceback.format()[-1].strip())
print("traced now/peak:", tracemalloc.get_traced_memory())
tracemalloc.stop()

go deeper

for a junior

Be ready to recite the three calls without hesitating: tracemalloc.start(), tracemalloc.take_snapshot(), then Snapshot.statistics('lineno'). Know that the result is sorted biggest-first and names a file and line.

for a middle

Explain the mechanics: what a Statistic's size and count mean, why count matters as much as size, what the key_type argument groups by, and why only allocations after start() are recorded.

for a senior

Show judgment about when a snapshot answers the question at all — a transient spike needs get_traced_memory()'s peak, not a snapshot, and attribution is only useful once you have narrowed the suspect region.

for a principal

Own the policy: which services ship with tracing available behind a flag, what the overhead budget is, and how a memory investigation is expected to proceed so every team is not inventing its own approach.

### What the module actually does `tracemalloc` is a standard-library module that hooks CPython's memory allocators and records, for every block still alive, *where in your source it was allocated*. That is the distinction worth holding on to: other memory tools tell you **how much** memory is in use, `tracemalloc` tells you **which line asked for it**. When a script's memory surprises you, the number alone is rarely actionable; the file and line number is. ### The three-call recipe ```python import tracemalloc tracemalloc.start() result = build_catalogue() # the work under suspicion snapshot = tracemalloc.take_snapshot() for stat in snapshot.statistics("lineno")[:10]: print(stat) tracemalloc.stop() ``` `tracemalloc.start()` switches tracing on. `tracemalloc.take_snapshot()` freezes the current set of traced blocks into a `Snapshot` object — a plain data structure you can keep in a variable, write to disk with `Snapshot.dump()` and read back with `Snapshot.load()`. `Snapshot.statistics(key_type)` groups those blocks and returns them sorted from largest to smallest. ### Reading a Statistic Each element of that list is a `tracemalloc.Statistic` with three attributes: * `size` — total bytes still allocated by the grouped traces; * `count` — how many separate blocks those bytes are spread over; * `traceback` — a `tracemalloc.Traceback`, whose `format()` method returns printable source lines. The `size`/`count` pair carries more information than either alone. Ten megabytes in one block is a single big buffer; ten megabytes spread over 400,000 blocks is a great many small objects, and the fix for those two situations is completely different — one is about a buffer's lifetime, the other about per-object overhead or a container that is never trimmed. ### The key_type argument `statistics()` takes a grouping key. `'lineno'` groups by file **and** line and is the normal choice. `'filename'` collapses a whole module into one row, which is useful when you first want to know *which* module is responsible. `'traceback'` groups by the entire recorded call stack, which is what you want when the same helper — a shared deserialisation function, say — is called from many places and you need to know which caller is driving the allocation. `'traceback'` is only informative if you started tracing with more than one frame per trace: `tracemalloc.start(nframe)` defaults to `nframe=1`, so by default every trace is a single frame and the grouping degenerates. ### What it will not show you Three blind spots trip people up on their first run. 1. **Nothing allocated before `start()`.** Tracing has no retroactive view. If the object you care about was created at import time and you started tracing in `main()`, it simply is not in the snapshot. Start tracing as early as you can — see the environment variable option below — or accept that you are measuring growth rather than totals. 2. **Nothing already freed.** A snapshot describes blocks *currently* alive. A function that allocates half a gigabyte and releases it before the snapshot leaves no trace at all; `tracemalloc.get_traced_memory()` returning a `(current, peak)` tuple is the way to catch a transient spike, and `tracemalloc.reset_peak()` (added in 3.9) lets you zero the high-water mark between phases. 3. **Nothing allocated outside CPython's allocators.** A compiled extension that calls the system allocator directly for its own buffers is invisible here, however large those buffers are. ### Cost, and turning it on without editing code Tracing is not free: every allocation does extra bookkeeping, and the traces themselves consume memory, which `tracemalloc.get_tracemalloc_memory()` reports. Expect a noticeable slowdown on allocation-heavy code, so treat it as a diagnostic mode rather than something left on by default. You do not have to edit the program to enable it. `python -X tracemalloc=25 script.py`, or the equivalent `PYTHONTRACEMALLOC=25` environment variable, starts tracing before your first line runs and keeps 25 frames per trace — the reliable way to catch allocations made during import, and the reason error messages that mention a missing traceback tell you to re-run with it. ### Where it fits Reach for `tracemalloc` when you can name a suspicious region of code and want the allocation attributed to a line. A single snapshot answers "what is holding memory right now"; comparing two snapshots answers "what is growing", which is the shape of a real leak hunt. Finding out *why* a block is still reachable — which object still refers to it — is a separate step with different tools.

  • What does the key_type argument to tracemalloc's Snapshot.statistics change?
    `'lineno'` groups traced blocks by file and line, `'filename'` collapses each module into a single row, and `'traceback'` groups by the whole recorded call stack. The last one is only useful if you started tracing with `tracemalloc.start(nframe)` for some nframe above the default of 1, otherwise every trace holds one frame and the grouping is identical to `'lineno'`.
  • What does tracemalloc.get_traced_memory() return, and when would you use it instead of a snapshot?
    It returns a `(current, peak)` tuple of bytes traced since tracing started. Use it when you only need totals — asserting a function stays under a memory budget in a test, or logging a high-water mark per request. `tracemalloc.reset_peak()` zeroes the peak between phases so one early spike does not mask later ones. A snapshot is what you take when you need attribution rather than a number.
  • Why might a large allocation not appear in a tracemalloc snapshot at all?
    Three reasons: it happened before `tracemalloc.start()`, so it was never recorded; it was already freed, and a snapshot only lists blocks currently alive; or it was made by compiled code calling the system allocator directly rather than going through CPython's allocators, which tracing never sees.

It is the difference between a bathroom scale and an itemised receipt: the scale says you are ten kilos heavier, the receipt says which line of the shopping list bought it.

saying these in an interview costs you the question

  • Thinks tracemalloc reports the process's resident memory
  • Expects to see objects allocated before start() was called
  • Believes a snapshot includes memory already freed
  • Reads one snapshot as proof of a leak
  • Assumes tracing is free enough to leave on permanently

context