Blog

is vs == in Python

== asks whether two objects have the same value. is asks whether they are the same object in memory.

They agree often enough that using the wrong one usually works, which is exactly what makes it a hard bug when it does not.

The difference

Two lists with the same contents are equal and are not identical.

# == compares values, is compares identity
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b, a is b)

c = a
print(a == c, a is c)

It prints:

True False
True True

Assign one to the other and both are true, because there is only one list.

Use is for None

None is a singleton. There is exactly one of it, so identity is the right question.

# for None, always use is
value = None
print(value is None, value == None)

It prints:

True True

== None usually works and is still wrong, for the reason in the next section.

Equality can be redefined

A class can make == mean anything. is cannot be overridden.

# == can be redefined, is cannot
class Always:
    def __eq__(self, other):
        return True

x = Always()
print(x == 'anything', x == 42)
print(x is Always())

It prints:

True True
False

If this object were compared with == None, the check would pass and your None handling would run on a real object.

Why small numbers look identical

CPython caches small integers, and constants inside one code object are shared. That makes is return True when you would expect False.

# small integers are cached by CPython
m, n = 256, 256
print('256:', m is n)
p = 1000
q = 1000
print('1000 literals:', p is q)          # same code object, so the constant is shared
r = int('1000')
print('1000 at runtime:', p is r, p == r)

It prints:

256: True
1000 literals: True
1000 at runtime: False True

The last line is the honest one. Two 1000s built the same way share an object here, but a 1000 built at runtime does not. None of this is guaranteed by the language, so never write comparisons that depend on it.

Strings do the same thing

String literals are interned. Strings built at runtime are not.

# the same happens with short strings
s1 = 'hello'
s2 = 'hello'
print('literal:', s1 is s2)
s3 = ''.join(['hel', 'lo'])
print('built at runtime:', s1 is s3, s1 == s3)

It prints:

literal: True
built at runtime: False True

This is why if name is "admin" passes in testing and fails in production, where the name arrives from a request.

Identity is what tells you if something was mutated

This is the useful side of is. It shows whether an operation made a new object or changed the one you had.

# identity is what actually changed
first = [1, 2]
second = first
first = first + [3]         # rebinds, new object
print(first, second, first is second)

third = [1, 2]
fourth = third
third += [3]                # mutates in place
print(third, fourth, third is fourth)

It prints:

[1, 2, 3] [1, 2] False
[1, 2, 3] [1, 2, 3] True

+ built a new list and rebound the name, so the second name still sees the old value. += changed the list in place, so both names see it. Same result on screen, different behaviour for anyone else holding a reference.

What to remember

  • == compares values, is compares identity.
  • Use is only for None, True, False and sentinels.
  • Caching of small ints and literal strings is a CPython detail. Do not build logic on it.
  • is is the tool for checking whether something was copied or mutated.

Linters flag is against a literal for this reason. If yours does not, turn that check on.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

Leave a Reply