Coding tips

Python is vs == (Identity vs Equality): Differences, the 256 Integer Cache, None Checks and SyntaxWarning Explained (Tested Examples)

In Python, == checks whether two values are equal, while is checks whether two names point to the very same object. Here is how they differ with lists, integers, strings, None and NaN, with real output from Python 3.13, plus the SyntaxWarning you get for is with a literal.

By 10 min read 0 views
Cover image for Python is vs == (Identity vs Equality): Differences, the 256 Integer Cache, None Checks and SyntaxWarning Explained (Tested Examples)

In Python, == asks "do these two have the same value?" and is asks "are these the exact same object in memory?". Two separate lists with the same items are == but not is. Use == for comparing values, including numbers and strings, and use is only for singletons such as None, True and False, or a sentinel object you created yourself.

The confusing part is that is sometimes seems to work for numbers and strings, because CPython reuses some objects behind the scenes. Code that depends on that can break when the value changes from 256 to 257. The tests below show exactly where the line is.

All output comes from Python 3.13.5 on my machine, with the SyntaxWarning example also run on Python 3.10 and 3.14.8.

In this guide:

  • The difference in one example
  • Why 256 is 256 but 257 is not 257
  • Why the same code can give a different answer
  • Strings and interning
  • SyntaxWarning: "is" with a literal
  • Always use is None (and why)
  • True, False and 1
  • NaN: the value that is not equal to itself
  • Classes: == depends on __eq__
  • A good use of is: sentinel objects
  • is vs == compared
  • Frequently asked questions

The difference in one example

a = [1, 2, 3]
b = a
c = [1, 2, 3]
print("a == c:", a == c)
print("a is c:", a is c)
print("a is b:", a is b)
print(id(a) == id(b), id(a) == id(c))
a == c: True
a is c: False
a is b: True
True False

a and c hold equal lists, so == is True. They are still two separate objects, so is is False. b = a did not copy anything, so a is b is True, and id() confirms it. The Python expressions reference defines is as an object identity test, true only when both sides are the same object.

That "same object" relationship matters when you change data. If two names share one list, a change through one shows up in the other. My post on assignment vs shallow copy vs deep copy shows that in detail.

Why 256 is 256 but 257 is not 257

CPython keeps a cache of small integer objects and hands out the same object every time. To see it without the compiler getting in the way, I created the numbers at runtime with int():

x = int("256")
y = int("256")
print("256:", x == y, x is y)
x = int("257")
y = int("257")
print("257:", x == y, x is y)
x = int("-5")
y = int("-5")
print("-5:", x == y, x is y)
x = int("-6")
y = int("-6")
print("-6:", x == y, x is y)
256: True True
257: True False
-5: True True
-6: True False

Every pair is ==, but is is only True from -5 to 256. Outside that range, each int() call builds a new object. The cache is an implementation detail of CPython, not a language rule, so other Python implementations can behave differently and so can future versions. The Python expressions reference only promises that is tests object identity, never which values share an object.

Why the same code can give a different answer

To make things more confusing, numbers written directly in your code are often shared even when they are large:

a = 1000
b = 1000
print("same file constants:", a is b)

def make():
    return 1000

print("from a function:", make() is a)
print("computed:", (a + 1) - 1 is a)
same file constants: True
from a function: True
computed: False

The compiler stored one 1000 constant and reused it for a, b and the function, so is said True. A value computed at runtime, (a + 1) - 1, is equal but a new object. This is why a quick test in a script can "prove" that is works for numbers, and the same check fails later with real data from a file or an API.

Strings and interning

import sys
s1 = "hello"
s2 = "hello"
print("literals:", s1 is s2)
s3 = "".join(["hel", "lo"])
print("built at runtime:", s3 == s1, s3 is s1)
s4 = sys.intern(s3)
print("after sys.intern:", s4 is s1)
s5 = " ".join(["hello", "world"])
s6 = "hello world"
print("with a space:", s5 == s6, s5 is s6)
literals: True
built at runtime: True False
after sys.intern: True
with a space: True False

Identical string literals were shared, but a string built with join() was a new object until I passed it through sys.intern(). The last line shows the same result for a string with a space. The sys module documentation describes sys.intern() as a speed trick for dictionary lookups, not a way to make is safe. For text from users, files or APIs, always compare with ==.

SyntaxWarning: "is" with a literal

Python warns you when you compare with is against a literal number or string, because it is almost always a bug:

status = int("200")
if status is 200:
    print("200: OK")

code = int("1000")
if code is 1000:
    print("1000: OK")
else:
    print("1000: not OK?")
e05_syntaxwarning.py:2: SyntaxWarning: "is" with 'int' literal. Did you mean "=="?
  if status is 200:
e05_syntaxwarning.py:6: SyntaxWarning: "is" with 'int' literal. Did you mean "=="?
  if code is 1000:
200: OK
1000: not OK?

Both lines got a warning, and the warning was right: the check passed for 200, which is cached, and failed for 1000, which is not. The warning was added in Python 3.8, as the What's New in Python 3.8 page notes. Here is the same file on two other versions:

Python 3.10:

e05_syntaxwarning.py:2: SyntaxWarning: "is" with a literal. Did you mean "=="?
  if status is 200:
e05_syntaxwarning.py:6: SyntaxWarning: "is" with a literal. Did you mean "=="?
  if code is 1000:
200: OK
1000: not OK?

Python 3.14.8:

e05_syntaxwarning.py:2: SyntaxWarning: "is" with 'int' literal. Did you mean "=="?
  if status is 200:
e05_syntaxwarning.py:6: SyntaxWarning: "is" with 'int' literal. Did you mean "=="?
  if code is 1000:
200: OK
1000: not OK?

Python 3.10 says "is" with a literal, while 3.13 and 3.14 name the type, 'int' literal. The warning appears when Python compiles the file, before any code runs. The fix is always the same: use ==.

Always use is None (and why)

PEP 8, the official style guide, says comparisons to singletons like None should use is or is not. There is a practical reason. A class can make == return anything:

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

w = Weird()
print("w == None:", w == None)
print("w is None:", w is None)
w == None: True
w is None: False

Weird claims to equal everything, so w == None is True, which is wrong. is cannot be overridden, so w is None gives the honest answer. Speed is a minor bonus. In my timing test, with 2 million checks and the best of 5 runs, x is None took 8.2 ns per check and x == None took 11.7 ns. Both are fast, so readability and correctness are the real reasons.

If a value can be None when you do not expect it, you will meet errors like 'NoneType' object is not subscriptable, and an early if value is None check is the usual fix.

True, False and 1

print("1 == True:", 1 == True)
print("1.0 == 1:", 1.0 == 1)
flag = 1
print("flag is True:", flag is True)
print("bool(flag) is True:", bool(flag) is True)
print("() is ():", tuple() is tuple())
print("[] is []:", list() is list())
1 == True: True
1.0 == 1: True
flag is True: False
bool(flag) is True: True
() is (): True
[] is []: False

True equals 1 because bool is a subclass of int, but 1 is True is False. In most code, write if flag: rather than comparing with True at all. The last two lines show another CPython detail: the empty tuple is shared, while each empty list is new, because lists are mutable.

NaN: the value that is not equal to itself

nan = float("nan")
print("nan == nan:", nan == nan)
print("nan is nan:", nan is nan)
values = [nan]
print("nan in [nan]:", nan in values)
print("float('nan') in [nan]:", float("nan") in values)
nan == nan: False
nan is nan: True
nan in [nan]: True
float('nan') in [nan]: False

Floating-point NaN is defined as not equal to anything, itself included. The surprise is the third line: nan in [nan] is True, because list membership checks identity first and equality second. A different NaN object is not found. To test for NaN, use math.isnan(), as the math module documentation describes, and never ==.

Classes: == depends on __eq__

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

p1 = Point(1, 2)
p2 = Point(1, 2)
print("p1 == p2:", p1 == p2)
print("p1 is p2:", p1 is p2)

class Plain:
    def __init__(self, x):
        self.x = x

print("Plain(1) == Plain(1):", Plain(1) == Plain(1))
p1 == p2: True
p1 is p2: False
Plain(1) == Plain(1): False

A dataclass generates an __eq__ that compares fields, so two points with the same coordinates are equal. A plain class without __eq__ falls back to identity, so Plain(1) == Plain(1) is False. If you want value comparison for your own class, define __eq__ or use a dataclass. JavaScript has a similar split between loose and strict comparison, covered in my guide to == vs === in JavaScript, but note that Python's is is not the same as ===.

A good use of is: sentinel objects

Sometimes None is a valid value, and you need a different way to say "no argument given". A private sentinel object, compared with is, solves it:

_MISSING = object()

def get(d, key, default=_MISSING):
    if key in d:
        return d[key]
    if default is _MISSING:
        raise KeyError(key)
    return default

print(get({"a": None}, "a"))
print(get({}, "a", None))
try:
    get({}, "a")
except KeyError as e:
    print("KeyError:", e)
None
None
KeyError: 'a'

The function can return a stored None, accept None as a default, and still raise KeyError when nothing was given, because only _MISSING itself is _MISSING. The standard library uses this pattern too. For more on missing keys, see my KeyError guide.

is vs == compared

==is
Question it asksDo these have the same value?Are these the same object?
Can a class change it?Yes, through __eq__No
[1, 2] ? [1, 2]TrueFalse
Use with None, True, FalseWorks but not recommendedRecommended (PEP 8)
Use with numbers and stringsYesNo, results depend on caching
Speed in my test (x is None)11.7 ns8.2 ns

The rule of thumb: if you are asking about the value, use ==. If you are asking "is this the one specific object", which is really only None, True, False or your own sentinel, use is. The PEP 8 programming recommendations give the same advice for None, and id() is handy if you ever need to check identity by hand while debugging.

Frequently asked questions

What is the difference between is and == in Python?

== checks whether two values are equal and can be customised with __eq__. is checks whether two names refer to the same object in memory and cannot be customised.

Why does 256 is 256 return True but 257 is 257 return False?

CPython caches integer objects from -5 to 256 and reuses them. Larger integers created at runtime are new objects, so is returns False even though == returns True.

Should I use is None or == None?

Use is None. PEP 8 recommends it, it cannot be fooled by a custom __eq__, and it was slightly faster in my test.

Can I use is to compare strings?

No. Identical strings are sometimes the same object because of interning, but strings built at runtime usually are not. Always compare strings with ==.

What does SyntaxWarning: "is" with a literal mean?

You compared something with a number or string literal using is. Python 3.8 and later warn because the result depends on object caching. Replace is with ==.

Is Python is the same as JavaScript ===?

No. JavaScript === compares values strictly without type conversion. Python is checks object identity. Python == behaves closer to JavaScript === for most built-in types.

Why is nan == nan False in Python?

IEEE 754 floating point defines NaN as not equal to anything, including itself. Use math.isnan(x) to check for NaN.

Last checked: 9 November 2026. Figures, settings and guidance can change, so confirm anything important with the sources linked above.

Share X LinkedIn WhatsApp Facebook
Was this helpful?

0%