Python Tricks: “is” or “==”
The answer behind an existential question, in python!

Have you ever gotten the warning in Pycharm that says:
Comparison with None performed with equality operators
and wondered why?
Well, today we are going to get to the bottom of the mystery once and for all!
Be prepared, this is actually quite a deep question that might cause you to question the meaning of life.
Is it Python?
Yes yes yes!
Python is known to have two operators: “is” and “==”, and in many conventional cases they produce the same results. But, without understanding the difference between them, you might run into obscure bugs that will make you scratch your head for hours.
Here’s an example:
from copy import copy
def compare(a, b):
print("--- compare a and b")
print(f"id(a): {id(a)}")
print(f"id(b): {id(b)}")
print(f"a == a: {a == a}")
print(f"a == b: {a == b}")
print(f"a is a: {a is a}")
print(f"a is b: {a is b}")
class Python:
def __init__(self, size):
self.size = size
def __eq__(self, other):
return self.size == other.size
print('test 1')
a = Python(5)
b = Python(6)
compare(a, b)
print('test 2')
a = Python(5)
b = Python(5)
compare(a, b)
print('test 3')
a = Python(5)
b = copy(a)
compare(a, b)
print('test 4')
a = Python(5)
b = a
compare(a, b)
We have a very simple class, called Python, it has an “__eq__” operator, that returns True if the other object to compare with has the same size.
Test 1: instantiate two Pythons of size 5, 6.
Test 2: instantiate two Pythons of size 5.
Test 3: instantiate one Python for a, make a copy for b.
Test 4, instantiate one Python for a, then assign it to b.
Below are the results:
test 1
--- compare a and b
id(a): 2810649485760
id(b): 2810649485664
a == a: True
a == b: False # different sizes
a is a: True
a is b: False
test 2
--- compare a and b
id(a): 2810648609840
id(b): 2810648608832
a == a: True
a == b: True
a is a: True
a is b: False
test 3
--- compare a and b
id(a): 2810649485760
id(b): 2810649485808 # different ids
a == a: True
a == b: True
a is a: True
a is b: False
test 4
--- compare a and b
id(a): 2810648609840
id(b): 2810648609840 # same ids
a == a: True
a == b: True
a is a: True
a is b: True
Looking at the results of test 1 and test 2, we notice that “a == b” is false for test 1, while true for test 2. This is understandable because python a and b have different sizes for test 1.
Looking at the results of test 3 and test 4, we see that “a is b” is false for test 3, but true for test 4. This is strange, since in both test 3 and test 4, a and b have size 5. But take a look at the id’s of a and b, in test 3, the id’s are different, while in test 4 they are the same.
From these tests, we can conclude that:
“==” operator calls “__eq__” function to determine equality, while “is” operator compares object id to determine equality.
Is Python a Dragon?
Depends

Let’s define another class, Dragon, and compare it with python:
class Dragon:
def __init__(self, size):
self.size = size
print('test 5')
a = Python(5)
b = Dragon(5)
compare(a, b)
print('test 6')
a = Dragon(5)
b = Python(5)
compare(a, b)
print('test 7')
a = Dragon(5)
b = Dragon(5)
compare(a, b)
results:
test 5
--- compare a and b
id(a): 2810649489264
id(b): 2810649488544
a == a: True
a == b: True
a is a: True
a is b: False
test 6
--- compare a and b
id(a): 2810648806784
id(b): 2810648806112
a == a: True
a == b: True
a is a: True
a is b: False
test 7
--- compare a and b
id(a): 2810649489264
id(b): 2810649488544
a == a: True
a == b: False
a is a: True
a is b: False
The results for “is” is as expected, it’s false for all three cases since the id’s are different in each case.
The results for “==” is a little strange, in all three cases the sizes of dragon and python are 5, yet when we do “python == dragon” and “dragon == python”, the result is true, while “dragon == dragon” is false.
This is actually due to how Python and Dragon classes are defined. Look closely, we see that Dragon doesn’t have an “__eq__” operator defined. This means that when we test “python == dragon” and “dragon == python”, the python “__eq__” operator is called, but when we test “dragon == dragon”, since neither side has an “__eq__” operator, the comparison defaults to “is”, which compares the id’s of the two dragons, resulting in false.

The situation gets even more interesting if we do define an “__eq__” operator for Dragon:
class Dragon2:
def __init__(self, size):
self.size = size
def __eq__(self, other):
return self.size * 2 == other.size
print('test 8')
a = Python(5)
b = Dragon2(5)
compare(a, b)
print('test 9')
a = Dragon2(5)
b = Python(5)
compare(a, b)
The dragons are a conceited species, when they compare themselves to others, they imagine themselves to be twice as big:
test 8
--- compare a and b
id(a): 2810649486096
id(b): 2810649487920
a == a: True
a == b: True
a is a: True
a is b: False
test 9
--- compare a and b
id(a): 2810628730688
id(b): 2810649488544
a == a: False
a == b: False
a is a: True
a is b: False
So in this case “Python(5) == Dragon2(5)” results in true, while “Dragon2(5) == Python(5)” results in false!
Is 0 equal to 0?
Always
Now that we understood how “is” and “==” works, let’s test out an important default type, the integers:
print('test 10')
a = int(0)
b = int(0)
compare(a, b)
print('test 11')
a = int(0)
b = a
compare(a, b)
print('test 12')
a = int(0)
b = copy(a)
compare(a, b)
results:
test 10
--- compare a and b
id(a): 2810573515184
id(b): 2810573515184
a == a: True
a == b: True
a is a: True
a is b: True
test 11
--- compare a and b
id(a): 2810573515184
id(b): 2810573515184
a == a: True
a == b: True
a is a: True
a is b: True
test 12
--- compare a and b
id(a): 2810573515184
id(b): 2810573515184
a == a: True
a == b: True
a is a: True
a is b: True
You see, the python creators have been working very hard at hiding the difference between “is” and “==” from you. For integers, as long as the values are the same, using “is” and “==” results doesn’t make a difference.
Is it None or is it equal to None?
Let’s not confuse ourselves

Let’s get to the None, so why does Pycharm warn you to not use “==” when comparing to None?
It’s really to protect you from making careless mistakes. Since None is a constant, it will always have only one instance, so there’s no reason to use “==” operator, which attempts to call the “__eq__” operator, unless you actually want to do something really strange like below:
class EndOfWorld:
def __eq__(self, other):
return other is None
a = EndOfWorld()
a == None
# Out[58]: True
Ship of Theseus
Let’s talk philosophy
So, python creators seems to have made a decision, every object has an id, if the id’s are the same, then two objects “is” each other. This is an interesting parallel to the thought experiment of “Ship of Theseus”, where the question of identity is raised by asking whether a ship with all of its components replaced is still the same ship.
See wiki on Ship of Theseus.
Well, it turns out, all you need is an id!
(What’s my id?)
Conclusion
I hope my readers had a great time learning the nuance in python object identity comparison. Theses concepts are not unique to python, they exist in C and C++ in the forms of reference and pointers, and similarly in many other languages, so it’s worth the time to understand the details to avoid mistakes.
Although it is quite easy to assign an id to everything in the abstract universe of programing languages, it is not so easy to figure out an id for objects in real life, what do you think?