Python ·
Python is vs == on integers: True at 256, False at 1000
is compares identity, not value. CPython caches ints from -5 to 256, so small numbers pass an is-check by luck and bigger ones fail it. Use ==.
Test yourself before reading - the write-up below gives it away.
# Did the request land on the configured port?
def same_port(configured, active):
return configured is active
print(same_port(int("80"), 80))print(same_port(int("8080"), 8080))# expected: True, True
# actual: True, False (!)
Two equal ports, one failing check
same_port(int("80"), 80) prints True. same_port(int("8080"), 8080) prints
False. Same function, values that compare equal, opposite answers.
# Did the request land on the configured port?
def same_port(configured, active):
return configured is active
print(same_port(int("80"), 80))
print(same_port(int("8080"), 8080))
# expected: True, True
# actual: True, False (!)
Why does is return False for equal integers?
is tests object identity - whether two names point at the same object in memory -
never value equality. CPython keeps one shared object for every integer from -5 to
256, so int("80") returns the cached 80 and the check passes by coincidence.
int("8080") builds a fresh object, and is says no to equal values. The fix:
compare numbers with ==.
The small-integer cache, and why 256 is the magic number
CPython preallocates the integers -5 through 256 at startup and hands out the same
object every time one of those values comes up - the docs state it plainly in the
C-API notes on integer objects:
"the current implementation keeps an array of integer objects for all integers
between -5 and 256". Any expression that produces 80 - parsing it, adding 79+1,
slicing it out of a struct - yields the identical object, so is agrees with ==
across that whole range. At 257 the guarantee ends. Each new 8080 is its own
object, identity diverges from equality, and line 3 - the is - is where the bug
lives. The int() calls on lines 5-6 look like the suspicious part and are doing
honest work.
There is a second layer that makes this bug genuinely slippery to reproduce: the compiler folds equal constants inside one code object. Test it in a script and you get a surprise in the other direction:
$ python -c "print(1000 is 1000)"
<string>:1: SyntaxWarning: "is" with 'int' literal. Did you mean "=="?
True
True - because both literals were folded into a single constant. The same line
typed as two separate REPL statements can print False. So the failure depends
not just on the value but on where each integer object came from, which is why an
is-comparison can survive a long time on numbers that "should" have failed it.
Values that arrive from parsing, arithmetic, or I/O - like production IDs - are
the ones that finally break it.
Green tests, then a cache that never hits
A session service checks if session.user_id is cached.user_id before reusing a
connection. The test fixtures create users 1 through 10 - all deep inside the
cached range, so every assertion passes and the branch looks correct in review
too, because is reads like English. In production, user IDs are seven digits.
The identity check fails for every request, the service treats each one as a cache
miss, and nothing errors - the only symptom is a hit rate near zero and a database
working ten times harder than capacity planning said it would.
The fix
def same_port(configured, active):
return configured == active
== asks the question the code meant to ask. Keep is for what it is designed
for: None and other true singletons (is None, is True in rare flag-object
protocols), where identity is the actual contract. Since Python 3.8 the compiler
backs this up - is against a literal raises the SyntaxWarning shown above, a
warning worth promoting to an error in CI (python -W error::SyntaxWarning).
How to find is-comparisons on values in your codebase
grep -rnE 'is (not )?-?[0-9]' --include='*.py' .catches literal comparisons; variable-to-variable cases need the linters below.- flake8 flags
isagainst str/bytes/int literals as F632; pylint calls it literal-comparison / R0123. - Runtime check while debugging: if
a == banda is not b, you are looking at two equal objects - which is the normal case, not a bug.
Related bugs
Java has this exact trap wearing different syntax: the Integer boxing cache runs
from -128 to 127, and == on boxed values works until it doesn't.
For another Python operation that does something adjacent to what its name says,
see str.strip(".txt"). The rest live at the
bug write-ups hub.
Port 80 will keep passing that check forever. That is the problem.