runtime
179 / 250
lesson 179 Memory & Python Internals · advanced

Measuring memory: getsizeof, tracemalloc snapshots and __slots__

Memory problems are found by measuring, not guessing. sys.getsizeof tells you about one object, tracemalloc tells you which lines allocated memory, and __slots__ shrinks objects you create by the million.

You'll be able to
  • Explain why sys.getsizeof of a container excludes the objects it holds
  • Compare two tracemalloc snapshots to find the lines that allocated the most
  • Use __slots__ for many small objects, and know what it gives up
The idea

sys.getsizeof(obj) returns the bytes one object occupies. For a container it is shallow: a list's size is its header plus one slot (a reference) per item. The items themselves live elsewhere and are not counted, so a list of long strings and a list of short strings with the same length report the same size. Exact numbers depend on the Python version and the machine (references are 4 bytes on a 32-bit build like this browser's, 8 on most desktops), so compare sizes rather than memorising them.

Run each example to see what it prints. Running examples doesn't count toward progress.

example-1.py · A list's size doesn't include its items
import sys

short_names = ["Ana", "Ben", "Chen"] * 1000
long_names = ["Alexandria Konstantinopoulou-Whitfield"] * 3000
print(len(short_names) == len(long_names))
print(sys.getsizeof(short_names) == sys.getsizeof(long_names))
print(sys.getsizeof("Ana") < sys.getsizeof("Alexandria Konstantinopoulou-Whitfield"))

To find where memory goes, tracemalloc records every allocation together with the line that made it. Take a snapshot before and after some work; after.compare_to(before, "lineno") lists the lines whose memory grew, biggest first. Each entry has a size_diff (bytes) and a traceback whose first frame has the lineno.

example-2.py · Which line allocated the memory?
import tracemalloc

tracemalloc.start()
before = tracemalloc.take_snapshot()
trips = [{"trip_id": i, "fare": 12.5} for i in range(20000)]
labels = [f"trip-{i}" for i in range(20000)]
after = tracemalloc.take_snapshot()
tracemalloc.stop()
for stat in after.compare_to(before, "lineno")[:2]:
    frame = stat.traceback[0]
    print("line", frame.lineno, "grew by more than 500 KiB:", stat.size_diff > 500 * 1024)

A common fix for *millions of small objects* is __slots__. Normally every instance carries its own attribute dictionary (__dict__). Listing the attribute names in __slots__ gives each instance fixed slots instead, which saves memory per object, at a price: the class can't get new attributes at run time.

example-3.py · Ten thousand taxi trips, with and without slots
import tracemalloc

class Trip:
    def __init__(self, trip_id, fare):
        self.trip_id = trip_id
        self.fare = fare

class SlimTrip:
    __slots__ = ("trip_id", "fare")

    def __init__(self, trip_id, fare):
        self.trip_id = trip_id
        self.fare = fare

def traced_bytes(make):
    tracemalloc.start()
    kept = make()
    current, peak = tracemalloc.get_traced_memory()
    tracemalloc.stop()
    return current

plain = traced_bytes(lambda: [Trip(i, 12.5) for i in range(10000)])
slim = traced_bytes(lambda: [SlimTrip(i, 12.5) for i in range(10000)])
print(slim < plain, plain / slim > 1.2)
print(hasattr(Trip(1, 9.5), "__dict__"), hasattr(SlimTrip(1, 9.5), "__dict__"))
example-4.py · Slots refuse new attributes
class SlimTrip:
    __slots__ = ("trip_id", "fare")

    def __init__(self, trip_id, fare):
        self.trip_id = trip_id
        self.fare = fare

trip = SlimTrip(16126, 40.8)
trip.tip = 10.7

When to use which: getsizeof for a quick look at one object (remember it is shallow); tracemalloc snapshots to find which lines allocate (and get_traced_memory() for current and peak totals); __slots__ only for classes with very many instances. Mistakes to avoid: believing a container's getsizeof is the memory of everything in it; measuring once on tiny data; and adding __slots__ everywhere, which makes classes rigid for no measurable gain.

Review
  • sys.getsizeof is shallow: a container's own size, not its contents.
  • tracemalloc snapshots + compare_to(…, "lineno") show which lines grew; get_traced_memory() gives current and peak.
  • __slots__ removes the per-object __dict__: less memory for many instances, but no new attributes.
  • Next: what CPython does when it runs your code, bytecode and the GIL.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

Measuring memory: getsizeof, tracemalloc snapshots and __slots__ — Memory & Python Internals · Runtime