Two questions that look the same but aren't
Every Python developer eventually writes if x is "something" or if count is 0 and
is surprised when it sometimes works and sometimes doesn't. The root cause is that ==
and is ask completely different questions — and CPython's internal optimisations quietly
make is appear correct on certain values, hiding the real distinction until production.
Quick-reference comparison
== (equality) | is (identity) | |
|---|---|---|
| Question asked | "Do these have the same value?" | "Are these the exact same object in memory?" |
| Calls | __eq__ on the left operand | Compares id() — no method call |
| Two equal objects | True | False (usually) |
| Same object | True | True (always) |
| Can be overridden? | Yes — __eq__ can return anything | No — pure identity, unoverridable |
| Use for | Values, data, content | None, True, False, sentinels |
What == does
== calls __eq__ on the left operand. The object decides what "equal" means. Two
completely separate objects with the same content compare as equal:
a = [1, 2, 3]
b = [1, 2, 3]
a == b # True — same contents
a is b # False — two different objects in memory
id(a) == id(b) # False — different addresses
Because __eq__ is a method, it can be overridden to return anything — including lying:
class Trickster:
def __eq__(self, other): return True # always "equal"
t = Trickster()
t == None # True — misleading!
t is None # False — identity can't be fooled
What is does
is compares object identity — specifically id(a) == id(b). In CPython, id() is
the memory address. Two names refer to the same object if and only if they point at the
same address.
a = [1, 2, 3]
b = a # b is an alias for a, not a copy
b is a # True — same object
b == a # True — same contents too
c = a[:] # shallow copy — new object
c is a # False — new list
c == a # True — same contents
The interning trap — when is appears to work on values
CPython pre-creates singleton objects for performance. This makes is return True on
certain values even when comparing two separate "literals" — a dangerous coincidence that
breaks outside the cached range:
# Small integers (-5 to 256) are cached singletons
a = 100; b = 100
a is b # True — same cached object (lucky!)
a = 300; b = 300
a is b # False — outside the cache, two objects
# Short identifier-like strings are auto-interned
x = "hello"; y = "hello"
x is y # True — auto-interned (lucky!)
x = "hello world!"; y = "hello world!"
x is y # often False — not auto-interned
x == y # True — always the correct comparison
Why this is dangerous: code with is 100 passes all tests (small values) and silently
breaks in production with larger inputs.
The correct uses of is
is is correct exactly when you're checking against a true singleton — an object of
which only one instance can ever exist:
# None is a singleton — is is idiomatic and unambiguous
if result is None:
...
# PEP 8 mandates is/is not for None and the bool singletons
if flag is True: # rarely needed — usually just `if flag:`
...
if flag is not False:
...
# Custom sentinels — identity is the whole point
_MISSING = object()
def get(mapping, key, default=_MISSING):
value = mapping.get(key, _MISSING)
if value is _MISSING: # distinguishes "key absent" from "value is None"
return default
return value
is is also correct as a fast-path optimisation inside __eq__ itself:
class Point:
def __eq__(self, other):
if self is other: # same object → trivially equal
return True
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
The one-rule decision guide
Use
==to compare values. Useisonly forNone,True,False, and your own sentinel objects.
If you're ever tempted to write is with a number, string, list, or any other value type,
replace it with ==. If swapping is for == would change the behaviour on any input,
you should have used ==.
# Wrong — relies on interning coincidence
if name is "admin": # SyntaxWarning in Python 3.8+
...
# Right
if name == "admin":
...
# Wrong
if count is 0:
...
# Right
if count == 0:
...
# Right — singletons only
if response is None:
...
Recap
== tests value equality by calling __eq__ — use it for any comparison of data or
content. is tests object identity by comparing id() — use it exclusively for
None, the bool singletons, and custom sentinels. CPython's integer cache (−5..256) and
string interning make is appear to work on plain values inside the cache range, but this
is a coincidence that breaks at the boundary and in non-CPython implementations. PEP 8
mandates is None / is not None; everything else should be ==.
Deep dive: Identity, is vs == & Interning — interview questions