Understanding memory consumption is essential for optimizing Python applications, especially when working with large datasets or memory-constrained environments. Python provides two primary methods for measuring object size: sys.getsizeof() and the __sizeof__() method. While they appear similar, they measure subtly different things.
This guide explains both approaches, their differences, and the important limitations you need to understand for accurate memory profiling.
Using sys.getsizeof()
The sys.getsizeof() function is the standard and recommended way to measure an object's memory footprint. It returns the size in bytes, including the garbage collector overhead required by Python's memory management system:
import sys
my_list = [1, 2, 3, 4, 5]
my_dict = {"name": "Alice", "age": 30}
my_string = "Hello, World!"
print(f"List size: {sys.getsizeof(my_list)} bytes") # List size: 104 bytes
print(f"Dict size: {sys.getsizeof(my_dict)} bytes") # Dict size: 184 bytes
print(f"String size: {sys.getsizeof(my_string)} bytes") # String size: 62 bytesYou can also provide a default value for objects that don't support size measurement:
size = sys.getsizeof(some_object, default=0)Using the sizeof() Method
The __sizeof__() method returns the object's base size as defined by its C implementation, excluding garbage collector overhead:
import sys
data = [10, 20, 30]
gc_size = sys.getsizeof(data)
base_size = data.__sizeof__()
print(f"With GC overhead: {gc_size} bytes") # With GC overhead: 88 bytes
print(f"Base size only: {base_size} bytes") # Base size only: 72 bytes
print(f"GC overhead: {gc_size - base_size} bytes") # GC overhead: 16 bytesThe Shallow Size Limitation
Both methods measure only shallow size: the container's memory, not its contents. This is a critical concept to understand:
import sys
small_items = [1, 2, 3]
large_items = ["x" * 1000000, "y" * 1000000, "z" * 1000000]
print(f"Small items list: {sys.getsizeof(small_items)} bytes") # Small items list: 88 bytes
print(f"Large items list: {sys.getsizeof(large_items)} bytes") # Large items list: 80 bytesThe sizes appear similar because lists store pointers to objects, not the objects themselves. Each pointer is typically 8 bytes regardless of what it points to.
Calculating Deep Size
To measure total memory including nested objects, use a recursive approach:
import sys
def deep_getsizeof(obj, seen=None):
"""Recursively calculate total size of object and contents."""
if seen is None:
seen = set()
obj_id = id(obj)
if obj_id in seen:
return 0
seen.add(obj_id)
size = sys.getsizeof(obj)
if isinstance(obj, dict):
size += sum(deep_getsizeof(k, seen) + deep_getsizeof(v, seen)
for k, v in obj.items())
elif isinstance(obj, (list, tuple, set, frozenset)):
size += sum(deep_getsizeof(item, seen) for item in obj)
return size
nested_data = {
"users": ["Alice", "Bob", "Charlie"],
"scores": [95, 87, 92],
"metadata": {"version": "1.0", "count": 3}
}
print(f"Shallow size: {sys.getsizeof(nested_data)} bytes") # Shallow size: 184 bytes
print(f"Deep size: {deep_getsizeof(nested_data)} bytes") # Deep size: 1146 bytesMethod Comparison
| Feature | sys.getsizeof() |
__sizeof__() |
|---|---|---|
| GC overhead | Included | Excluded |
| Reliability | Works on all objects | May not be implemented |
| Accuracy | Real-world footprint | Implementation detail |
| Measurement depth | Shallow | Shallow |
Practical Memory Profiling
For comprehensive memory analysis, consider these approaches:
import sys
def deep_getsizeof(obj, seen=None):
... # as defined in example above
def format_size(bytes_size):
"""Convert bytes to human-readable format."""
for unit in ['B', 'KB', 'MB', 'GB']:
if bytes_size < 1024:
return f"{bytes_size:.2f} {unit}"
bytes_size /= 1024
return f"{bytes_size:.2f} TB"
def profile_object(obj, name="Object"):
"""Display memory profile for an object."""
shallow = sys.getsizeof(obj)
deep = deep_getsizeof(obj)
print(f"{name}:")
print(f" Shallow: {format_size(shallow)}")
print(f" Deep: {format_size(deep)}")
print(f" Ratio: {deep/shallow:.1f}x")
profile_object([i for i in range(1000)], "List of 1000 integers")
profile_object({i: str(i) for i in range(1000)}, "Dict with 1000 items")Output:
List of 1000 integers:
Shallow: 8.65 KB
Deep: 35.99 KB
Ratio: 4.2x
Dict with 1000 items:
Shallow: 36.09 KB
Deep: 114.10 KB
Ratio: 3.2xUnderstanding the distinction between shallow and deep size, and choosing the appropriate measurement method, enables accurate memory profiling and effective optimization of your Python applications.