Home Tutorials Core Syntax

Core Syntax

Python Comparison and Identity: == vs is, and How Equality Really Works

Pyford Notes July 6, 2026 6 min read
Key points
  • == 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
Important: This behaviour is an implementation detail, not a language guarantee, and it can differ between Python versions or implementations. Never rely on 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.