Why Python's is isn't equals
It's pretty easy to mix up is
and ==
when learning Python.
The difference seems subtle at first and in many cases the two
operators seem to be doing the exact same thing.
one = 1
if one == 1:
print("one == 1")
if one is 1:
print("one is 1")
The difference between the two, however, is important and misusing is
can lead to perplexing bugs. If you don't read any further, the
most important thing to take away is
- Never use
is
to compare a variable to a literal value, always use==
- Using
is
to check if a variableis None
is the only correct usage
The fact that one is 1
works above should be thought of as a quirk in
Python's implementation, but incorrect code.
Python's is
operator means these two things point to the same location
in memory while ==
means these two things store the same value. This
is important because if a is b
then changing the state of
a
also changes b
, while no such expectation should be made
with ==
. It is obvious that if a is b
then a == b
, but the reverse
is not true.
An example to illustrate:
list_1 = [1, 2]
list_2 = list_1
if list_1 == list_2:
print("list_1 == list_2")
if list_1 is list_2:
print("list_1 is list_2")
list_1.append(3)
print(f"list_1: {list_1}")
print(f"list_2: {list_2}")
Note that updating list_1
also updates list_2
. We can also see that
they have the same id
print(f"id(list_1): {id(list_1)}")
print(f"id(list_2): {id(list_2)}")
Why this is important¶
Given what we've looked at so far, it seems strange that one is 1
above returned True because it implies that the variable one
is stored
at the same location as the value 1
. Indeed, this is exactly what happens
and it's an optimization done by CPython when storing booleans, small ints,
and short strings. The optimization is most common in storing strings and
is called string interning.
This optimization, however, can lead to confusing scenarios like the following:
one = 1
one is 1
one_thousand = 1000
one_thousand is 1000
short_string = "short"
short_string is "short"
not_so_short_string = "not so short string"
not_so_short_string is "not so short string"
Note the difference in behavior depending on the size of the value being stored. Even more confusing, however, is that this is implementation-specific. Switching between 32 and 64-bit CPython (and to a different implementation altogether) yields different behaviors.
I once spent several hours trying to figure out why a script that worked
on one Windows PC did not work at all on another. The script was sending
data over a serial port so I was quite sure that there was either a
hardware issue or a misconfigured setting somewhere. It turned
out that the working PC had 64-bit Python installed while the troublesome
PC had a 32-bit install and a string comparison (quite far from where
I was searching for the issue) using is
was behaving differently.
Parting thoughts¶
Most of the conda environments that I have floating around at the moment are Python 3.7.
While writing this, I created a new conda environment for testing and learned that
Python 3.8 now emits a SynaxWarning on erroneous usage of is
.
This is an excellent idea and I'm glad that it made its way into CPython. Pylint
also provides warnings against this and is yet another reason to use static
analysis tools.
I didn't really address the fact that is None
is the correct way to do
comparisons to None
. There is a good reason for it and it's explained in
this Real Python post under Taking
a Look Under the Hood. The short explanation is that None
is an immutable
singleton in Python so all copies of it will point to the same thing. Using
== None
generally also works, although for reasons also outlined in the
Real Python post, it is not the same and has a small potential to produce very
confusing bugs. This distinction is important but understandably confusing
at first.
There are a few other caveats to things that I've written here. For example,
it is possible for an is
comparison to evaluate to True while an ==
to evaluate to False by doing something like:
class NeverEquals:
def __eq__(self, other):
return False
a = NeverEquals()
b = a
print(f"a is b: {a is b}")
print(f"a == b: {a == b}")
Although this would be quite a strange scenario.