'is' vs '==' in Python - Object Comparison

'is' vs '==' in Python

Python has two very similar operators for checking whether two objects are equal. These two operators are is and ==.

They are usually confused with one another because with simple data types, like ints and strings (which many people start learning Python with) they seem to do the same thing:

x = 5
s = "example"

print("x == 5: " + str(x == 5))
print("x is 5: " + str(x is 5))
print("s == 'example': " + str(s == "example"))
print("s is 'example': " + str(s is "example"))

Running this code will result in:

x == 5: True
x is 5: True
s == 'example': True
s is 'example': True

This shows that == and is return the same value (True) in these cases. However, if you tried to do this with a more complicated structure:

some_list = [1]

print("some_list == [1]: " + str(some_list == [1]))
print("some_list is [1]: " + str(some_list is [1]))

This would result in:

some_list == [1]: True
some_list is [1]: False

Here it becomes obvious that these operators aren't the same.

The difference comes from the fact that is checks for identity (of objects), while == checks for equality (of value).

Here's another example that might clarify the difference between these two operators:

some_list1 = [1]
some_list2 = [1]
some_list3 = some_list1

print("some_list1 == some_list2: " + str(some_list1 == some_list2))
print("some_list1 is some_list2: " + str(some_list1 is some_list2))
print("some_list1 == some_list3: " + str(some_list1 == some_list3))
print("some_list1 is some_list3: " + str(some_list1 is some_list3))

This results in:

some_list1 == some_list2: True
some_list1 is some_list2: False
some_list1 == some_list3: True
some_list1 is some_list3: True

As we can see, some_list1 is equal to some_list2 by value (they're both equal to [1]]), but they are not identical, meaning they aren't the same object, even though they have equal values.

However, some_list1 is both equal and identical to some_list3 since they reference the same object in memory.

Mutable vs Immutable Data Types

While this part of the problem now might be clear (when we have named variables), another question might pop up:

How come is and == behave the same with unnamed int and string values (like 5 and "example") but don't behave the same with unnamed lists (like [1])?

There are two kinds of data types in Python - mutable and immutable.

  • Mutable data types are data types which you can "change" over time
  • Immutable data types stay the same (have the same memory location, which is what is checks) once they are created

Mutable data types are: list, dictionary, set, and user-defined classes.

Immutable data types are: int, float, decimal, bool, string, tuple, and range.

Like many other languages Python handles immutable data types differently than mutable types, i.e. saves them in memory only once.

So every 5 you use in your code is the exact same 5 you use in other places in your code, and the same goes for string literals you use.

If you use the string "example" once, every other time you use "example" it will be the exact same object. See this Note for further clarification.

We will be using a Python function called id() which prints out a unique identifier for each object, to take a closer look at this mutability concept in action:

s = "example"
print("Id of s: " + str(id(s)))
print("Id of the String 'example': " + str(id("example")) + " (note that it's the same as the variable s)")
print("s is 'example': " + str(s is "example"))

print("Change s to something else, then back to 'example'.")
s = "something else"
s = "example"
print("Id of s: " + str(id(s)))
print("s is 'example': " + str(s is "example"))
print()

list1 = [1]
list2 = list1
print("Id of list1: " + str(id(list1)))
print("Id of list2: " + str(id(list2)))
print("Id of [1]: " + str(id([1])) + " (note that it's not the same as list1!)")
print("list1 == list2: " + str(list1 == list2))
print("list1 is list2: " + str(list1 is list2))

print("Change list1 to something else, then back to the original ([1]) value.")
list1 = [2]
list1 = [1]
print("Id of list1: " + str(id(list1)))
print("list1 == list2: " + str(list1 == list2))
print("list1 is list2: " + str(list1 is list2))

This outputs:

Id of s: 22531456
Id of the String 'example': 22531456 (note that it's the same as the variable s)
s is 'example': True
Change s to something else, then back to 'example'.
Id of s: 22531456
s is 'example': True

Id of list1: 22103504
Id of list2: 22103504
Id of [1]: 22104664 (note that it's not the same as list1!)
list1 == list2: True
list1 is list2: True
Change list1 to something else, then back to the original ([1]) value.
Id of list1: 22591368
list1 == list2: True
list1 is list2: False

We can see that in the first part of the example, s returned to the exact same "example" object it was assigned to at the beginning, even if we change the value of s in the meantime.

However, list does not return the same object whose value is [1], but a whole new object is created, even if it has the same value as the first [1].

If you run the code above, you are likely to get different IDs for the objects, but the equalities will be the same.

When are 'is' and '==' Used Respectively?

The is operator is most commonly used when we want to compare the object to None, and restricting its usage to this particular scenario is generally advised unless you really (and I do mean really) want to check whether two objects are identical.

Besides, is is generally faster than the == operator because it simply checks for integer equality of the memory address.

Important note: The only situation when is works exactly as might be expected is with singleton classes/objects (like None). Even with immutable objects, there are situations where is does not work as expected.

For example, for large string objects generated by some code logic, or large ints, is can (and will) behave unpredictably. Unless you go through the effort of interning (i.e. making absolutely sure that only one copy of a string/int/etc. exists), all the various immutable objects you plan to use, is will be unpredictable.

The bottom line is: use == in 99% of cases.

If two objects are identical they are also equal, and that the opposite isn't necessarily true.

Overriding '==' and '!=' Operators

Operators != and is not behave in the same way as their "positive" counterparts do. Namely, != returns True if objects don't have the same value, while is not returns True if the objects are not stored in the same memory address.

One other difference between these two operators is that you can override the behavior of ==/!= for a custom class, while you can't override the behavior of is.

If you implement a custom __eq()__ method in your class, you can change how the ==/!= operators behave:

class TestingEQ:
    def __init__(self, n):
        self.n = n

    # using the '==' to check whether both numbers
    # are even, or if both numbers are odd
    def __eq__(self, other):
        if (self.n % 2 == 0 and other % 2 == 0):
            return True
        else:
            return False


print(5 == TestingEQ(1))
print(2 == TestingEQ(10))
print(1 != TestingEQ(2))

This results in:

False
True
True

Conclusion

In short, ==/!= check for equality (by value) and is/is not check whether two objects are identical, i.e. checks their memory addresses.

However, avoid using is unless you know exactly what you're doing, or when dealing with singleton objects like None, since it can behave unpredictably.

Author image
Hey guys, I want to point out that I don't have any social media to avoid mistakes. If you need any help - post it in the comments :)