DEV Community

Cover image for Why 'is' Lies to You in Python: 256 is 256, but 257 isn't (and 3.15 Changed It)
Muiduzzaman Lipu for Refactorly

Posted on

Why 'is' Lies to You in Python: 256 is 256, but 257 isn't (and 3.15 Changed It)

In Python 3.9, 256 is 256 is True, but 257 is 257 is False. And in Python 3.15, the answer changed. Here's why, and the real bug it causes.

Video version:

The weird part

Python 3.9, typed line by line in the REPL:

>>> a = 256
>>> b = 256
>>> a is b
True
>>> x = 257
>>> y = 257
>>> x is y
False   # 🤯
>>> x == y
True
Enter fullscreen mode Exit fullscreen mode

Same numbers, different answers.

== compares values, is compares identity

  • a == b asks: do they have the same value?
  • a is b asks: are they the same object in memory? (like id(a) == id(b))

Two lists with the same items are equal, but they're two different objects:

>>> [1, 2] == [1, 2]
True
>>> [1, 2] is [1, 2]
False
Enter fullscreen mode Exit fullscreen mode

Why 256 "works": the small integer cache

To save memory, CPython creates small integers once and reuses them, so every 256 is the same object. Outside that range, each new int is usually a new object.

I measured the cached range by parsing numbers with int("…") until is stopped returning True:

Python cached range
3.9.6 -5 to 256
3.14.10 -5 to 256
3.15.2 -5 to 1024

So in Python 3.15:

>>> x = 257
>>> y = 257
>>> x is y
True      # changed in 3.15!
>>> x = 1025
>>> y = 1025
>>> x is y
False
Enter fullscreen mode Exit fullscreen mode

Code that uses is on numbers can give a different result just by upgrading Python. This is a CPython implementation detail, not a language rule, which is exactly why you should never rely on it.

Gotcha: writing x = 257; y = 257 on one line (or in the same file) can give True even on old versions, because the compiler reuses identical constants in the same code block. That makes this bug even harder to spot.

The real bug: works in tests, fails in production

def check(rows, expected):
    total = len(rows)
    if total is expected:        # ❌ identity, not value
        return "OK"
    return "MISMATCH"

check(["u"] * 1000, 1000)   # 'OK' on 3.15 (cached)
check(["u"] * 1500, 1500)   # 'MISMATCH' 😱
Enter fullscreen mode Exit fullscreen mode

Your tests with 1,000 rows pass. Production with 1,500 rows fails, even though the numbers are equal. The fix is simply ==.

Python even warns you

If you use is with a literal, Python emits a warning:

SyntaxWarning: "is" with 'int' literal. Did you mean "=="?
  if x is 257:
Enter fullscreen mode Exit fullscreen mode

Don't ignore it.

When to use is

✅ Use is for:

  • x is None / x is not None
  • sentinels and singletons (x is MISSING)
  • when you really mean the same object

❌ Use == for numbers, strings, lists, dicts: any value comparison.

Why is None instead of == None? A class can override == to return anything:

class Weird:
    def __eq__(self, other):
        return True

w = Weird()
w == None   # True  (lies!)
w is None   # False (can't be faked)
Enter fullscreen mode Exit fullscreen mode

Go search your code for is followed by a number or string literal. 👀


I post one tested coding tip every day on YouTube at Refactorly. The 30-second version of this one is here.

Top comments (0)