How Python Manages Memory: Understanding the Memory Usage of Python Objects
Photo by Sigmund on Unsplash Everything is an object in Python, I know this is not the first time you have heard this term and it won’t be the last. This...
Everything is an object in Python, I know this is not the first time you have heard this term and it won’t be the last. This article is not entirely about Python Objects or any Python Object in particular, unless you came with a mat, then be my guest.
My article is with respect to how much memory your Python objects really use. In a recent project in the #ALX SE course, I was asked how many “int” objects were created after the first line of this code is executed:
print("I")
print("Love")
print("Python")I was momentarily flummoxed because I hadn’t created any “int” objects myself, but Python apparently took the liberty of doing so without my permission. How dare it! Don’t get me wrong, I’m not shouting, but yeah, it definitely did.
So it turns out that with CPython implementation, a range of 262 int(s) are created and cached. This process is known as interning. Also, two constants are created: NSMALLNEGINTS (between -5 to -1) and NSMALLPOSINTS (0 to 256).
Did you know that each int Object in Python takes up a whopping 28 bytes of memory? It’s true, and it might make you wonder how that’s possible. But here’s the thing: in Python, everything is an object, even the int. So while in C/C++, an int is a simple type, in Python, it’s a dynamic type that consists of a struct with pointers and other types in it.
Now, don’t freak out just yet! When you create an instance of an int in Python, the interpreter automatically allocates the necessary memory on the Heap and even frees it up when it’s no longer needed. That means you don’t have to worry about memory management like you would in C/C++. No more manual malloc and frees, folks! Thanks to Python’s Garbage Collector, you can sit back, relax, and let Python do the heavy lifting of keeping the Heap memory clean and removing objects that are no longer in use. Isn’t that a relief? ☺

Now comes the hard truth, NSMALLNEGINTS, and NSMALLPOSINTS are 262 int objects that Python creates and caches for you to use. So whenever you execute Python code in the current implementation, 8,352 bytes of memory are allocated and cached. Don’t take my word for it, I may be wrong.
Let’s look into it. Each int is 28 bytes — the getsizeof() function uses the sizeof() function under the hood — and an additional 8 bytes of pointer to the next address. That’s 32 bytes, so the difference in the memory location of -5 and -4 is 32 bytes. Mathematically, 32 * 261 = 8,352.
>>> import sys
>>> sys.getsizeof(5)
28
>>> id(-6)
140119147077584
>>> id(-5)
9801056
>>> id(256)
9809408
>>> id(257)
140119147077584
>>> 9809408 - 9801056
8352From my terminal, we can see that the address of -5 and that of 256 (this will be different from yours) is very different than other int Objects (9801056–9809408 = 8,352). This is because these int Objects were created and cached by Python, and whenever you initialize int variables that fall within this range (NSMALLNEGINTS and NSMALLPOSINTS), no new int Object is created.
Now you may be wondering why Python does this. Doesn’t this come with a memory cost? Yes, it does, but it’s a tradeoff for the time complexity of creating these numbers. Imagine each time we append a new int in a list within this range, and we have to create them on each call, that would have been less effective if you asked me.
>>> a = [1, 2, 3, 4]
>>> id(a[0])
9801248
>>> id(1)
9801248
>>> id(a[3])
9801344
>>> id(4)
9801344
>>> b = 1
>>> b is a[0]
TrueWhen you initialize int variables that fall within this range, no new int object is created. This process reduces memory allocation and optimizes time complexity since these int objects are frequently used in Python.
This same implementation is how Python manages memory when creating memory allocation for immutable objects like strings.
In conclusion, Python objects use more memory than you may think. However, CPython implementation caches commonly used int objects to reduce memory allocation and optimize time complexity. This memory management model is extended to immutable objects like strings as well. By understanding how Python manages memory and keeping track of your objects’ memory usage, you can optimize your code and reduce memory usage.
It’s important to keep in mind that Python takes care of memory allocation and garbage collection automatically, which is one of the many reasons why it’s a popular choice among developers. However, understanding how memory is managed under the hood can help you write more efficient and optimized code. By being aware of the memory usage of your Python objects, you can avoid unnecessary memory allocation and improve the performance of your code.