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 is to 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!

Thank You Image


You might also be interested in: