Kodeclik Blog
Python Pointers: Do we need them?
Pointers in Python are basically variables that store the memory addresses of other objects. All languages store data, instructions, and programs in memory and therefore they are stored at specific memory locations. Knowing how pointers work helps you understand a lot about a language’s implementation details.
Some languages like C and C++ make pointers to be a “first class” object meaning they give you many primitives and functions to work and reason with pointers. Languages like Python, on the other hand, abstract away details of pointers and a regular programmer will not need to “peek under the hood”. Nevertheless, Python provides many functions that will help us understand how the language manages its memory.
Consider the following simple program:
pi = 3.14
print(pi)
This produces the output:
3.14
as expected.
The id() function
Let us now try this:
pi = 3.14
print(pi)
print(id(pi))
This produces:
3.14
140067448715312
(If you try this, the second number that is printed might be different for you.) The function id(pi) returns the object’s memory address; here, the object is the variable “pi” and therefore it is returning the address where this object is stored.
Let us change the value of pi and redo the above statements:
pi = 3.14
print(pi)
print(id(pi))
pi = 3.1415
print(pi)
print(id(pi))
This yields:
3.14
140196188842032
3.1415
140196122305712
Notice two important differences. First the second line has printed a new address (not the address we saw earlier). This is because each time we run the program it is starting a fresh execution and allocating a fresh chunk of memory and therefore is using a new address (namely, 140196188842032) and not the old address (140067448715312). But more importantly, note that when I reassign pi to a new value (3.1415 instead of 3.14) and I print the id(pi) again, I get a new value! This new memory location is at: 140196122305712.
The reason this is happening is that variables like “pi” (in general, all integer, float, string, boolean variables) are immutable, meaning they cannot be changed. So when we re-assigned the value of pi we have essentially created a new object and made the variable “pi” point to it. Even though it might seem as if we have changed the value of pi (contradicting the property of immutability), we have in reality created a new object and made pi point to it. This will happen even if you do some operations on the variable, like so:
x = 3.14
print(x)
print(id(x))
x = x+2
print(x)
print(id(x))
This yields:
3.14
140021518096432
5.140000000000001
140021443167408
Note that the two addresses are different.
Let us consider the following program:
x = 3.14
print(x)
print(id(x))
y=x
print(y)
print(id(y))
This produces the following output:
3.14
139815069915184
3.14
139815069915184
Note that in this case the two addresses are the same. The memory location holding the number of interest (i.e., 3.14) is the same but two different variables point to it. If you were to re-assign y to something else, id(x) would give the same value but id(y) would be different:
x = 3.14
print(x)
print(id(x))
y=x
print(y)
print(id(y))
y=2
print(y)
print(id(y))
print(id(x))
This yields:
3.14
139899230287920
3.14
139899230287920
2
139899227129600
139899230287920
Note that id(x) is the same in both printings (before and after reassignment of y) but id(y) has changed (after reassignment).
Mutable data types
Let us try the following piece of code with lists:
animals = ['dog','cat','cow']
print(animals)
print(id(animals))
animals.append('elephant')
print(animals)
print(id(animals))
This gives the output:
['dog', 'cat', 'cow']
140123999519744
['dog', 'cat', 'cow', 'elephant']
140123999519744
Note what has happened. The list has been modified but the address of the list stays the same! This is because lists are mutable objects in Python. You can modify lists and change them and this is why the memory address stays the same. Let us try a few more operations to modify the list:
animals = ['dog','cat','cow']
print(animals)
print(id(animals))
animals.append('elephant')
print(animals)
print(id(animals))
animals[0] = "tiger"
print(animals)
print(id(animals))
animals[3] = "mule"
print(animals)
print(id(animals))
This gives:
['dog', 'cat', 'cow']
140667062215488
['dog', 'cat', 'cow', 'elephant']
140667062215488
['tiger', 'cat', 'cow', 'elephant']
140667062215488
['tiger', 'cat', 'cow', 'mule']
140667062215488
As expected the memory address does not change.
What are mutable data types in Python?
Lists, sets, and dictionaries are mutable in Python. Most other primitive data types, like integers, strings, floating point numbers, boolean, tuples, and complex numbers are immutable.
The is operator
Python has another useful function to check if two objects have the same memory address. Instead of printing the memory addresses and comparing them the is inflix operator compares two objects and determines if they share the same memory address. For instance:
animals = ['dog','cat','cow']
farmanimals = animals
print(animals is farmanimals)
yields:
True
Let us now try:
animals = ['dog','cat','cow']
farmanimals = animals
farmanimals.append('tiger')
print(animals is farmanimals)
This produces:
True
Wow - how can that be? I only modified farmanimals! Let us print the animals list and verify:
animals = ['dog','cat','cow']
farmanimals = animals
farmanimals.append('tiger')
print(animals is farmanimals)
print(animals)
This gives:
True
['dog', 'cat', 'cow', 'tiger']
This is because when we copy mutable objects (using our assignment statement above), the copying is done by reference (this is called “Copy by Reference”). As a result, a change in one object is reflected in the other object as well.
On the other hand when we copy immutable objects (like strings, variables) and change one of them a new object is created (as we saw using the id() function).
Note that the id() function is different from the == operator (the latter compares values whereas the former compares memory locations). For instance, consider the following code:
animals = ['dog','cat','cow']
farmanimals = animals
print(animals is farmanimals)
print(animals == farmanimals)
This produces:
True
True
This means the addresses as well (or, thus,) the contents of these addresses are the same. On the other hand, consider the following code:
animals = ['dog','cat','cow']
farmanimals = ['dog','cat','cow']
print(animals is farmanimals)
print(animals == farmanimals)
This produces:
False
True
This is because in the second line, when we created the farmanimals variable we initialized it separately, not with reference to the animals variable. As a result, the memory locations are different but the contents are the same.
So what have we learned in this blog post? Python pointers are useful to peek behind the scenes at how (and where) objects are allocated and what happens as these objects are modified over the course of a program. There are Python modules like ctypes that enable you to more directly work with pointers similar to languages like C/C++ but that is beyond the scope of this article. Also read about Python memory errors.
Interested in more things Python? See our blogpost on Python's enumerate() capability. Also if you like Python+math content, see our blogpost on Magic Squares. Finally, master the Python print function!
Want to learn Python with us? Sign up for 1:1 or small group classes.