Article
== vs is. Differences between keyword 'is' and '=='. Comparisons in Python.
Variables and Memory Addresses in Python
Python variable is bound to a memory address. And under a given memory address, we store an object.
The majority of newly created variables will receive a unique memory address but not all of them. Some variables will be bound to the one memory address with exactly the same object.
After all, what is the point of keeping in memory more than one 313 number or more than one
"devcuriosity" string?
Python memory optimization is a complex algorithm. It is hard to list all rules standing behind binding new variables to the existing memory addresses, but some of the general ideas would be: - Object needs to be immutable - Object needs to be a constant, known at the time Python parses the code. This allows the interpreter to recognize and deduplicate identical immutable objects during execution. - This optimization doesn't work in REPL (Python Standard Shell).
Python does that to optimize memory usage. Please keep in mind that there are some exceptions to these rules as well. In general, it can be dangerous to build your logic around those rules.
We need to remember that mutable objects behave differently. If declared objects are mutable (like lists, for example), they will always receive a new memory address. Python does that for us so we can modify one object without modifying another object (since those objects are mutable).
We can take a peek at memory addresses by combining methods id() and hex().
id() function returns a unique id number of a given variable. And hex() converts
that number into a hexadecimal string with "0x" at the beginning.
Take a look at that in action:
variable_1 = 313
variable_2 = 0
variable_3 = 313
print(hex(id(variable_1))) # 0x1040b0a50
print(hex(id(variable_2))) # 0x1040b0910
print(hex(id(variable_3))) # 0x1040b0a50
Output:
0x1040b0a50
0x1040b0910
0x1040b0a50
# It will vary on your machine but the first and third ones will be identical.
I created three variables, but only two memory addresses are used. That is because,
variable_1 and variable_3 are pointing to the same address, the address with integer 313.
Remember that with mutable objects, the effect will be different:
list_1 = ["a", "b", "c"]
list_2 = ["a", "b", "c"]
print(hex(id(list_1))) # 0x274a3f2f640
print(hex(id(list_2))) # 0x274a4382440
Output:
0x274a3f2f640
0x274a4382440
But what does it have to do with == and is?
When we are using == we are "just" comparing values (or equality of given variables).
But with is we are comparing memory addresses. So is is much more "exclusive".
Values need to be identical to result in True.
Let's take a look at something different with memory addresses as well:
list_1 = [1, 2, 3]
list_2 = [1, 2, 3]
print(hex(id(list_1))) # 0x1f069a0f640
print(hex(id(list_2))) # 0x1f069e62440
print(list_1 == list_2) # True
print(list_1 is list_2) # False
print(hex(id(list_1[0]))) # 0x1f0694800f0
print(hex(id(list_2[0]))) # 0x1f0694800f0
print(list_1[0] == list_2[0]) # True
print(list_1[0] is list_2[0]) # True
Output:
0x1f069a0f640
0x1f069e62440
True
False
0x1f0694800f0
0x1f0694800f0
True
True
In the first pair of comparisons, the == resulted in True because list_1 and list_2 are equal.
The magic method __eq__ was called, and it returned True because it decided that by its standards, those
two lists are equal to each other.
The is comparison resulted in False because list_1 and list_2 have different memory addresses. So
they are NOT the same, identical object.
In the second pair of comparisons, we are getting True in both cases. That is because at index 0 for
both lists we have an integer 1. So both indexes are pointing to the same memory address, with our 1.
And of course, if is comparison results in True, == comparison will be True as well. It doesn't
always work like that the other way!
(unless we will make our custom implementation of those mechanics but that wouldn't make a lot of sense, would it?)
As a rule of thumb, use is for identity checks (e.g., if x is None) and == for value comparisons.
Avoid using is for equality checks unless you are intentionally comparing memory addresses.
Be careful, is might be too exclusive even for you!
Please don't jump to the conclusion that an immutable data structure that contains other immutable objects will always result in having the same memory address! Some examples may give you some unexpected results. Take a look at that:
x = (1, 2)
y = (1, 2)
print(hex(id(x))) # 0x1c8dfce7180
print(hex(id(y))) # 0x1c8dfce7180
a = tuple((1, 2))
b = tuple([1, 2])
c = tuple(range(1, 3))
d = tuple(_ for _ in x)
print(hex(id(a))) # 0x1c8dfce7180
print(hex(id(b))) # 0x1c8e01428c0
print(hex(id(c))) # 0x1c8e0142680
print(hex(id(d))) # 0x1c8dfce4880
print(x, y, a, b, c, d) # (1, 2) (1, 2) (1, 2) (1, 2) (1, 2) (1, 2)
Output:
0x1c8dfce7180
0x1c8dfce7180
0x1c8dfce7180
0x1c8e01428c0
0x1c8e0142680
0x1c8dfce4880
(1, 2) (1, 2) (1, 2) (1, 2) (1, 2) (1, 2)
We declared 6 tuples in total, and all of them are simply (1, 2). You could expect that they would have the
same memory address, but it is NOT true! Only tuples named x, y, and a share the same address.
Tuples b, c, and d have different memory addresses. Memory optimization is a pretty complicated
process in Python. I wanted to show you these examples, so you won't be surprised when you are using
the is keyword in your code. Remember how exclusive is is and use it wisely.
As a final warning, please bear in mind that there is a small chance that in the runtime duration
of your program, a memory address will be reused to store a different object. The chances of that happening
are very low, but it is possible. At the same time, it is not possible for an object to change memory address
during its lifespan.
Always be cautious when using id() for comparisons in critical logic (I would say - avoid it if possible).
Practical Takeaways
-
Use
==to compare values for equality. -
Use
isto check if two variables refer to the same object (e.g.,if x is None). -
Avoid relying on memory addresses for logic, as they can lead to unpredicted behavior.
Final words
Please bear in mind that we have only scratched the surface, if you are further interested in this topic,
I would advise you to search for information about the Register design pattern, __eq__, __hash__ magic methods,
and memory optimization in Python. As always, when in doubt, reach out to official documentation and
maybe even to the implementation of a given object in CPython.
Thank you!
I hope you found this helpful! If you have any feedback, questions or requests, please reach out to me through Contact page!