What is the difference between `is` and `==` in Python, and when does `is` give a surprising result?
`==` calls `__eq__` and tests value equality. `is` tests object identity — whether two names point to the exact same object in memory. The surprise comes from CPython's interning: small integers (-5 to 256) and many short strings are cached, so `is` returns True even for separately created objects, but this is an implementation detail you must never rely on.
How to think about it
This is a classic probe of whether you understand Python’s object model, not just its syntax. Every value in Python is an object living somewhere in memory, and a variable is just a name pointing at one. == asks whether two objects represent the same thing; is asks whether two names point at the exact same object.
Picture two houses built from one floor plan. == asks “do the layouts match?” — yes. is asks “is this literally the same building?” — no. Concretely, == calls the object’s __eq__ method (which any type can override to define equality), while is compares identity directly — no method call, nothing to override.
a = [1, 2, 3]
b = [1, 2, 3]
c = a
a == b # True — same values
a is b # False — different objects in memory
a is c # True — c is just another name for the same list
A worked example — including the interning surprise
# Identity vs equality
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print("a == b :", a == b) # same values
print("a is b :", a is b) # different objects
print("a is c :", a is c) # same object, two names
print()
# CPython caches the small integers -5..256, so they're shared singletons.
# int("...") forces a fresh object each time, exposing the cache boundary:
print("256:", int("256") is int("256")) # True — 256 is cached
print("257:", int("257") is int("257")) # False — 257 is built fresh
print()
# The only correct use of `is`: comparing against a singleton
result = None
print("result is None:", result is None) # idiomatic and safe
a == b : True
a is b : False
a is c : True
256: True
257: False
result is None: True
That middle block is the trap interviewers love. 256 comes back True and 257 comes back False — not because of anything about the numbers, but because CPython happens to cache the integers from −5 to 256. It’s a memory optimisation, not a language guarantee.
Why interning exists
Those small integers — loop counters, lengths, flags — turn up constantly, so CPython pre-builds them once and hands out the same object every time, saving memory and construction cost. The same trick applies to many short, identifier-like strings fixed at compile time. But it is a runtime implementation detail: PyPy or Jython may differ, and even CPython can change it between releases. The honest one-liner is that is boils down to “do these two have the same identity?”, and identity is only stable for an object’s lifetime.
The only safe uses of is
if result is None: ...
if obj is NotImplemented: ...
# (and prefer a plain `if flag:` over `if flag is True:`)