Python Comparison and Identity: == vs is, and How Equality Really Works
- == asks whether two objects have the same value; is asks whether they are literally the same object in memory.
- For custom classes, == calls __eq__; without one defined, it falls back to identity, the same as is.
- Always use x is None (or is not None) rather than x == None.
- Small integers and some strings can appear identical due to interning, but this is an implementation detail, never something to rely on.
== checks value, is checks identity
== compares whether two objects have equal value; is compares whether two names refer to the exact same object in memory. Two different objects can have equal value without being the same object:
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True -- same contents
print(a is b) # False -- two distinct list objects
c = a
print(a is c) # True -- c refers to the same object as a
a and b were built from separate list literals, so even though their contents are identical, they occupy different memory. c = a does not copy the list — it makes c a second name for the same object, which is why a is c is true.
How __eq__ controls ==
For a plain custom class, == defaults to identity — the same as is — unless the class defines its own __eq__ method:
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2) # False -- no __eq__ defined, falls back to identity
class Point2:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return isinstance(other, Point2) and self.x == other.x and self.y == other.y
print(Point2(1, 2) == Point2(1, 2)) # True
This is why libraries provide helpers like @dataclass, which generates a sensible field-by-field __eq__ automatically instead of requiring you to write it by hand for every class.
Comparing with None
The idiomatic way to check for None is is None or is not None, never == None. There is exactly one None object for the lifetime of a program, so identity comparison is both correct and faster than a value comparison, and it sidesteps the rare case of a custom __eq__ that behaves oddly against None:
value = None
if value is None:
print("no value set")
Style guides for Python (PEP 8 among them) explicitly recommend is / is not for comparisons to singletons like None, True, and False.
Why small ints sometimes pass is
CPython, the standard Python implementation, caches small integers (typically -5 to 256) and some short strings as single shared objects for efficiency. This can make is appear to work for equality by accident:
a = 100
b = 100
print(a is b) # True in CPython -- small-int cache
x = 1000
y = 1000
print(x is y) # often False -- outside the cached range
is for value comparison of numbers or strings — always use == for that, and reserve is for genuine identity checks like is None.
Chained comparisons
Python allows chaining comparison operators, which reads naturally and evaluates each adjacent pair, combining the results with a logical and:
x = 5
print(1 < x < 10) # True, equivalent to (1 < x) and (x < 10)
print(1 < x < 3) # False, since x < 3 is False
Each operand in the chain is evaluated only once, which matters when an operand is an expression with a side effect — unlike manually writing 1 < f() and f() < 10, which would call f() twice.
Equality and hashability
Python's rule for dict keys and set members is that two objects considered equal by == must also produce the same hash value from hash(). This is why defining a custom __eq__ on a class silently disables the class's default hashing — Python cannot guarantee the rule holds unless you also define __hash__ yourself:
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return isinstance(other, Point) and self.x == other.x and self.y == other.y
p = Point(1, 2)
# hash(p) # raises TypeError: unhashable type -- __eq__ was defined, __hash__ was not
To make such a class usable as a dict key or set element again, add a matching __hash__ that is based on the same fields used in __eq__: def __hash__(self): return hash((self.x, self.y)). Mutable objects are usually left unhashable on purpose, since a hashable object's fields should not change after it has been placed in a set or used as a dict key — doing so would leave it in the wrong hash bucket and make future lookups fail silently.