is vs == and string/int interning
The gotcha
is compares identity (same object in memory). == compares value. CPython caches small ints and short strings, so is sometimes “works” by accident — until it doesn’t.
Minimal repro
a = 256; b = 256
print(a is b) # True (cached)
a = 257; b = 257
print(a is b) # False (not cached) — but in REPL same line might be True
# because the compiler folds constants per code object
s = "hello"; t = "hello"
print(s is t) # True (string literals are interned)
s = "hello world!"; t = "hello world!"
print(s is t) # often False — long/whitespace strings not auto-interned
Why it happens
CPython preallocates small int objects in range [-5, 256] (the “small int cache”) for performance — int(5) always returns the same object. String literals that look like identifiers are interned automatically; runtime-built strings usually aren’t.
These are CPython implementation details. PyPy, Jython, etc. behave differently. Never rely on this.
How to avoid
Use is only for: None, True, False, sentinels, and explicit identity checks (e.g. comparing two objects to see if they’re literally the same).
Use == for everything else — value comparison.
if x is None: # correct idiom
...
if x == None: # works but linted-out; relies on __eq__
...
if value is 5: # even if it works today, broken in principle
...
Interview angle
“What’s the difference between is and ==?” is a warm-up. The follow-up — “why does 1000 is 1000 return False but 100 is 100 returns True?” — separates candidates who memorized vs. understood.