import sys
items = [object() for _ in range(1000)]
sys.getsizeof(items) # 8856
8.8 KB for a thousand objects. That is the number that ends up in the capacity model, and it is wrong by a factor of three here — the thousand object() instances are 16 bytes each, so the real cost is around 24 KB. For a list of dataclass instances holding strings, the error is one to three orders of magnitude.
The documentation is not hiding this. getsizeof reports “only the memory consumption directly attributed to the object… not the memory consumption of objects it refers to”. A list is an array of pointers; its direct cost is the array. What the pointers point at belongs to those objects.
The same number can be right or wrong
sys.getsizeof([[1, 2, 3]] * 1000) # 8056
Here 8,056 bytes is correct and complete-ish, because [x] * 1000 creates one inner list referenced a thousand times. There genuinely is only one sublist. The identical measurement on a thousand distinct sublists would be the same 8,056 and would be a lie by roughly 100 KB.
You cannot tell which situation you are in from the number. That is the point: getsizeof answers a question about one object, and “how much memory does this data structure cost” is a question about a graph.
What list over-allocation looks like
getsizeof is genuinely useful for one thing — seeing the allocator’s own behaviour:
| length |
getsizeof |
|---|---|
| 1 | 88 |
| 5 | 120 |
| 9 | 184 |
| 17 | 248 |
Those steps match list_resize in Objects/listobject.c, whose documented growth pattern for the allocated capacity is 0, 4, 8, 16, 24, 32, 40, 52, 64, 76, … — roughly geometric with a small additive term, so append is amortised O(1). The empty list’s 56-byte header plus 8 bytes per allocated slot reproduces every row.
This is why list.append in a loop is fine and why list.insert(0, x) is not: growth is amortised at the end, and nowhere else.
💡A colleague estimates a cache's memory as n * sys.getsizeof(sample_entry). Name three distinct ways that estimate goes wrong, in both directions.
click to reveal
Under, by the referents. If an entry holds a dict, a list of strings, or another object, none of it is counted. This is usually the dominant error and it is unbounded — a “small” entry can hold a 2 GB model.
Under, by the container. The dict or list holding the entries has its own cost: pointers, and for a dict a hash table sized well above n with load-factor slack. Neither is in getsizeof(entry).
Over, by sharing. If entries share structure — interned strings, a common config object, a shared schema — multiplying a single entry’s size by n counts the shared part n times. For low-cardinality string fields this can be most of the apparent total.
And a fourth that is neither: getsizeof measures what the object reports, not what the process holds. Allocator size classes, arena fragmentation, free lists and any memory owned by a C extension are all invisible. A correct traversal still will not equal RSS.
Building the traversal correctly
The recursive sum has three constraints that are all easy to get wrong, and all three appear in the accompanying problem:
Count each distinct object once. Keyed on id(), because your objects are frequently unhashable and because == on a large container is a deep comparison that will also give you the wrong answer for two structurally-equal distinct objects.
Terminate on cycles. Same visited set, same reason as the collector’s.
Do not recurse into str. A str is a Sequence of str, and "h"[0] is "h", so a traversal that treats sequences uniformly either recurses forever or terminates by accident on single characters. str, bytes and bytearray are atomic for sizing purposes. This is the single most common bug in hand-written deep_sizeof implementations on the internet.
Dispatch over collections.abc — Mapping for keys and values, Sequence/Set for items — rather than a chain of concrete isinstance(x, dict) checks, so it works on defaultdict, OrderedDict, Counter and your own containers.
The honest limit
Even a perfectly correct recursive sum will not match RSS, and you should say so when you present the number. It ignores allocator overhead and size-class rounding, free lists, arena fragmentation, and any memory owned by a C extension — a NumPy array’s getsizeof includes its buffer, but a library holding a native handle to a 500 MB model reports 48 bytes.
For “how much does this structure cost”, a traversal is the right tool. For “why is this process using 3 GB”, it is not; that is tracemalloc (article 11.13) for Python allocations and memray for everything else.