Why this matters
Languages such as C use a struct to group several values under one name. Python has no struct keyword for this job. A class does it, and each object you create takes some memory. One object is small. A program that creates millions of them can run out of memory or spend a lot of time filling and clearing memory, so it helps to know how big an object is.
In C, a struct with a char and an int takes more bytes than the two values added together, because the compiler inserts unused bytes called padding. Python does not expose this detail in the same way. Each attribute of an object is a reference, and the interpreter decides where the references are stored. There is no padding between fields, and the order in which you write the attributes does not change the size. The sizes in this lesson are the ones CPython, the usual interpreter, uses on a 64-bit machine.
This lesson is about the size of one object. How objects are shared between names, and how unused objects are freed, belong to the lessons on references and leaks earlier in this module.
What takes the memory
A Python object has three kinds of memory.
The first is the object header. It is a small fixed part that the interpreter uses to track the object: its type, its reference count, and the memory that the cyclic collector needs for objects that can form cycles. You cannot see the header in your code.
The second is the attribute storage. An ordinary class instance keeps its attributes in a place for them, which is either a dictionary or, in recent versions, a block of slots that the interpreter manages. Each attribute holds a reference, and a reference is a fixed size, 8 bytes on this 64-bit build.
The third is the objects the attributes point to. A number, a string or a list that an attribute refers to has its own size. Changing the size of the referenced object does not change the size of the holder.
The function sys.getsizeof reports the size of one object, including its header and its direct storage. It does not add up the objects that it refers to. The attribute storage of an ordinary class instance is not always counted by it, so the next example compares it with a measurement that counts all the memory.
Type nothing into the Input box for this program.
int 0: 28
int 2**100: 40
empty list: 56
list of 3 items: 88
Empty(): 48
Plain(1, 2): 48The numbers depend on the Python version and the machine, so your numbers may differ. Two things are worth noticing. A larger integer takes more bytes, because Python integers have no fixed size. A list of 3 items is 88 bytes, which is 56 bytes for the empty list plus 8 bytes for each of its 3 references. The items themselves, the three integers, are not in that number.
Plain(1, 2) shows 48 bytes, the same as Empty(). That does not mean the two attributes are free. The attribute storage is separate from the object as sys.getsizeof counts it. The next section measures the whole cost.
Measuring the whole cost
The tracemalloc module counts every block of memory that the program allocates. Take a reading before a batch of objects is created and another after, then divide the difference by the number of objects. The result is the average cost of one object, including its attribute storage.
The next program makes 10,000 instances of two classes. One class stores its attributes in an ordinary way. The other declares __slots__, which is a class attribute that lists the attribute names, so the interpreter can store them in a fixed place with no dictionary.
import tracemalloc
class Plain:
def __init__(self, a, b):
self.a = a
self.b = b
class Slotted:
__slots__ = ("a", "b")
def __init__(self, a, b):
self.a = a
self.b = b
def bytes_per_object(factory, n=10_000):
tracemalloc.start()
before = tracemalloc.get_traced_memory()[0]
kept = [factory(i) for i in range(n)]
after = tracemalloc.get_traced_memory()[0]
tracemalloc.stop()
return (after - before) / n
print("Plain:", round(bytes_per_object(lambda i: Plain(i, i))))
print("Slotted:", round(bytes_per_object(lambda i: Slotted(i, i))))On the machine used to write this lesson, with Python 3.14.4, one run printed these lines:
Plain: 128
Slotted: 87The slotted class used about two-thirds of the memory of the plain class. The numbers include the 8 bytes per object that the list kept spends on its reference, and the integer that both attributes share, so they are a little higher than the object alone. Your numbers will differ, but the slotted class should be smaller on any CPython version that supports __slots__.
The measurement is an average. A single object of each kind would give a different number, because a fixed amount of memory is shared by all of them.
slots and what it gives up
A class with __slots__ has no __dict__ for each object. That is why it uses less memory, and it is also why it is less flexible. The check below shows both sides.
Type nothing into the Input box for this program.
has __dict__: False
a and b: 1 2
AttributeError: 'Slotted' object has no attribute 'z' and no __dict__ for setting new attributesThe object accepts only the names listed in __slots__. Setting any other name raises an AttributeError. This is a useful check for a class that should always have the same fields, because a typing mistake such as self.nmae = name becomes an error at once, and it does not silently create a new attribute.
The cost is that you cannot add attributes on the fly, and some tools that rely on __dict__ do not work with the class. Use __slots__ for a class that is created in large numbers and has a fixed set of fields. For a small number of objects, the saving does not matter, and the plain class is simpler.
The standard library's dataclass decorator can write the __slots__ line for you. The argument slots=True asks for it:
Type nothing into the Input box for this program.
Point(x=3, y=4)
FalseField order does not change the size
In C, the order of fields can change the size of a struct, because padding is added between fields that do not line up. Python has no padding between attributes. Two classes with the same attributes in a different order have the same size.
Type nothing into the Input box for this program.
TrueThe interpreter stores the attributes by name, not by their position in the source. So the order you write them in does not change the size. Choose the order that reads best. Reordering the attributes to save memory does nothing in Python.
Small numbers in a list
A list of plain Python integers stores a reference to each integer, and each integer is an object of its own. For many numbers of the same kind, the array module from the standard library stores the raw values in one block, with a fixed size per item.
Type nothing into the Input box for this program.
list of 1000 ints: 8056
array of 1000 ints: 4080
bytes per item in the array: 4The list counts only its own slots, 8 bytes each, so 8056 bytes. It does not count the 1000 integer objects, which each take more memory. The array stores 4 bytes per item and no objects, so its size is 4000 bytes for the items plus a small header. The array is a good choice when the values are numbers of one type and you need many of them.
Common mistakes
Adding up the attributes and forgetting the rest. The attributes of Plain are two references, but the object takes more memory than that, because of its header and its attribute storage. Measure with tracemalloc when the size matters.
Trusting sys.getsizeof for a whole structure. It reports one object. A list of a thousand strings reports the size of the list, which is small, and the strings take most of the memory. To find the total, add up the sizes of the items too, or measure with tracemalloc.
Believing that field order changes the size. The example above showed the same size for two orders. Writing the attributes in a different order does not save memory.
Using slots for a few objects. The saving is per object. A few hundred objects save very little. Use __slots__ when a program creates many objects of the same class.
Forgetting that slots rules the class. Once a class has __slots__, it cannot take new attribute names. A subclass that adds attributes gets a __dict__ again, unless it also declares __slots__. Read the class you are extending before you add slots.
Comparing numbers from different versions. The sizes above are from Python 3.14.4 on one machine. The header, the attribute storage and the integer sizes change between versions. Measure on the version your program runs on.
Practice
Exercise 1. Predict whether sys.getsizeof on an object of a class with two attributes is larger, smaller or equal to the size of an object of a class with no attributes. Then check it.
Show the answer
With the sizes in this lesson, the two are equal in sys.getsizeof, because the attribute storage is not counted in that number. The tracemalloc measurement shows that the class with attributes uses more memory in total. This is why sys.getsizeof alone can mislead you about an object.
Exercise 2. A class has the attributes name, age and city. A colleague adds zip by writing self.zip = z in one method, but the class uses __slots__ = ("name", "age", "city"). What error does the program raise, and what is the fix?
Show the answer
It raises AttributeError, because the object accepts only the names in __slots__. The message says the object has no attribute zip and no __dict__ for setting new attributes. The fix is to add "zip" to the tuple in __slots__.
Exercise 3. Write a function that returns the total size in bytes of a list of strings, counting the list and each string. Use sys.getsizeof for each one.
Show the answer
import sys
def total_size(items):
return sys.getsizeof(items) + sum(sys.getsizeof(x) for x in items)The function adds the size of the list and the size of each string. A string that appears twice in the list is counted twice, so the result can be larger than the real memory when the list holds the same string object more than once.
Exercise 4. Explain why Plain and Slotted in this lesson show different sizes in tracemalloc, even though sys.getsizeof gives 48 for both.
Show the answer
In this version of CPython, sys.getsizeof does not add the memory that holds the attributes of a Plain object to the reported size. The Slotted class has no such extra memory. The tracemalloc measurement counts every block that the program allocates, so it shows the difference.
Exercise 5. Why is the array of 1000 integers smaller than the list of 1000 integers, when both hold the same values?
Show the answer
The list holds one reference per item, 8 bytes each, and each integer is a separate object. The array stores the raw 4-byte values in one block, and it creates no objects for them. The array is smaller because it skips the reference and the integer object for each item.
Summary
- A Python object takes a header, its attribute storage and the memory of the objects its attributes refer to.
sys.getsizeofreports one object without the objects it refers to, and it may leave out an attribute storage that is kept apart from the object.tracemallocmeasures the real cost of many objects, and divides it by the number of objects.__slots__removes the attribute dictionary of each instance, which saves memory for many objects. It also stops new attributes from being added.- Python has no padding between attributes, so the order of attributes in the source does not change the size.
- A list of numbers takes a reference and an object for each item. The
arraymodule stores the raw values in one block.