is vs == in Python: Identity vs Equality Explained
6 min readBytePatterns
is vs == in Python: is checks whether two names point at the same object, == asks whether two values are equal. Small-int caching, interning and is None.
is vs == in Python in one line: a is b is true only when both names refer to the very same object, while a == b asks the objects whether their values are equal, by calling __eq__. Identity implies equality for almost every object; equality never implies identity. That is why == is the comparison you want for values, and is is reserved for a few singletons, above all None. The rules below are from the Python language reference and PEP 8, as of October 2026; every example runs on Python 3.9.
| Operator | Question it asks | Use it for |
|---|---|---|
a is b | same object? compares id(a) and id(b); cannot be overridden | None, sentinels, "did I get the same object back?" |
a == b | equal values? calls a.eq(b); defaults to identity | numbers, strings, containers, your own value types |
The problem it solves
Python names are labels on objects, not boxes holding values. Two labels can sit on one object, and two separate objects can hold equal values, so "are these the same?" has two answers. Code that picks the wrong one tends to pass every test on a developer's machine and fail somewhere else, because the results of is on numbers and strings depend on interpreter details that nobody promised.
The intuition
Every object has an identity, a type and a value. The identity never changes while the object is alive; in CPython id() is its memory address.
iscompares identities. It is a pointer comparison, the same cost every time, and no class can change what it means. It answers "are these two names for one object?"==compares values, and the object decides what that means. Python calls__eq__. Lists compare item by item, numbers by value, strings by characters. A class that defines no__eq__inheritsobject's default, which is identity: two separate instances are unequal even with identical attributes.- Defining
__eq__switches off hashing. A class that overrides__eq__without defining__hash__gets__hash__set toNone, so its instances cannot go in a set or be dict keys until you add a hash consistent with equality.
Then the trap. CPython keeps one object for each integer from -5 to 256 and reuses it, and it interns many strings, especially identifier-like literals. So x is 256 can be true while x is 257 is false, and two equal strings may or may not be one object. Both are documented as implementation details. Python 3.8 and later emit a SyntaxWarning when you write is against a literal, precisely because the result is not reliable.
None is the opposite case. There is exactly one None object, so x is None is both correct and immune to a strange __eq__. PEP 8 says comparisons to singletons like None should always use is or is not, never the equality operators.
When to use which
- Comparing values (numbers, strings, lists, dicts, dataclasses):
==. - Checking for
None:is Noneandis not None. - A private sentinel such as
MISSING = object():is, since only identity can tell it apart from user data. - "Did this function return the same object it was given?"
is, usually in a test. - Comparing with an int or string literal: never
is.
Watch it run
The lesson's animation builds the word STRING one character at a time, two ways. Because a Python string cannot be edited, each += in general copies what is already there into a fresh string, and the copy bill grows to 21 characters for six appended, while the list version appends and joins once. Read it with identity in mind: every one of those intermediate results is a new object with a new id(). Two strings that end up with the same text after different histories are equal and, in general, not identical, which is exactly why == is the only safe way to compare them.
String Basics
Step 1 of 9
Build STRING one character at a time, two ways. A Python string cannot be edited in place.
The same interactive animation as the lesson — step through it with the controls.
The code
Aliases against copies, the caching and interning details shown for what they are, and the warning the compiler gives for is with a literal:
import sys, warnings
a = [1, 2, 3]
b = a # a second name for the same object
c = [1, 2, 3] # an equal, separate object
print(a == b, a is b, id(a) == id(b)) # True True True
print(a == c, a is c, id(a) == id(c)) # True False False
b.append(4)
print(a, c) # [1, 2, 3, 4] [1, 2, 3]
# CPython details, not language rules: never write code that depends on them
print(int("256") is int("256")) # True <- small ints are cached
print(int("257") is int("257")) # False
literal = "byte"
built = "".join(["by", "te"]) # same text, built at run time
print(built == literal, built is literal) # True False
print(sys.intern(built) is literal) # True <- interned on request
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
compile("x = 5\nprint(x is 5)", "<demo>", "exec")
print(caught[0].category.__name__, caught[0].message)
# SyntaxWarning "is" with a literal. Did you mean "=="?
The numbers are built with int() at run time on purpose: two 257 literals in one file can be folded into a single constant, which would make the second line print True and teach the wrong lesson. Next, how __eq__ decides ==, why is None beats == None, and the one built-in value that is not equal to itself:
class Plain: # no __eq__: object's default is identity
def __init__(self, x):
self.x = x
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other): # == now compares values
if not isinstance(other, Point):
return NotImplemented
return (self.x, self.y) == (other.x, other.y)
class Anything:
def __eq__(self, other): # a badly behaved __eq__
return True
print(Plain(1) == Plain(1)) # False <- falls back to identity
print(Point(1, 2) == Point(1, 2), Point(1, 2) is Point(1, 2)) # True False
try:
{Point(1, 2)}
except TypeError as e:
print(e) # unhashable type: 'Point'
odd = Anything()
print(odd == None, odd is None) # True False <- why PEP 8 says "is None"
nan = float("nan")
print(nan == nan, nan is nan) # False True
print([nan] == [nan], nan in [nan]) # True True <- containers check identity first
Built-in containers assume an object is equal to itself, so they test identity before calling __eq__; that is why [nan] == [nan] is true while nan == nan is not. A seeded check over random ints, strings, lists, points and deliberate aliases: whenever a is b, a == b held, and many pairs were equal without being identical:
import random
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)
def make(rng):
kind = rng.choice(["int", "str", "list", "point"])
v = rng.randint(0, 3)
return {"int": lambda: int(str(v)), "str": lambda: "".join(["k"] * v),
"list": lambda: [v], "point": lambda: Point(v, v)}[kind]()
rng = random.Random(42)
ok, equal_not_same = True, 0
for _ in range(3000):
pool = [make(rng) for _ in range(6)]
pool += [rng.choice(pool) for _ in range(3)] # aliases: the same object twice
for a in pool:
for b in pool:
if a is b:
ok &= a == b # identity implies equality here
elif a == b:
equal_not_same += 1 # equality without identity
print(ok, equal_not_same > 0) # True True
nan = float("nan")
print(nan is nan, nan == nan) # True False <- the one exception
The complexity
is:O(1), one pointer comparison, always.==on numbers:O(1).==on strings, lists, tuples:O(n)in the worst case, item by item; it stops at the first difference, and lengths are compared first for some types.==on your own classes: whatever__eq__does.
The speed of is is not a reason to use it for values: an answer that is fast and sometimes wrong is not an optimisation.
Where it goes wrong
if count is 0:orif name is "admin":Works in a quick test because of caching or interning, fails for other values or other interpreters. Python 3.8 and later warn; treat the warning as an error.if x == None:Usually works, but a class with a permissive__eq__, or an array library whose==returns an array, gives a wrong or ambiguous answer. Writeis None.if not x:when you meantis None. Empty strings,0and empty lists are falsy too, so a valid value is treated as missing.- Adding
__eq__and losing hashability. Instances silently stop working as dict keys. Define__hash__from the same fields, or use a frozendataclass. - Comparing
id()values of short-lived objects. Two objects whose lifetimes do not overlap may get the sameid(), soid(f()) == id(g())can be true for different objects.
When it shows up in interviews
As a quick Python screening question, "what is the difference between is and ==?", followed by "why does a is b print True for 256 and False for 257?" or "why does PEP 8 want is None?". Strong answers name __eq__, call the caching an implementation detail, and mention the hash consequence. Why strings are rebuilt rather than edited, which is where many surprising identities come from, is covered in strings in Python: immutability and join and in the String Basics lesson.
How to say it in an interview
"is checks identity: are both names bound to the same object, which in CPython means the same address. == checks equality by calling __eq__, which defaults to identity but is overridden by lists, strings, numbers and my own value classes. So for values I always use ==. I use is only for singletons like None, as PEP 8 says, and for sentinels. Small integers and interned strings can make is look like it works on values, but that is a CPython detail, and Python warns when you write is with a literal."
Frequently asked questions
What is the difference between is and == in Python?
is returns True when both operands are the same object, comparing identities. == returns whatever the left operand's __eq__ decides, normally a comparison of values. Two separate lists with the same items are == but not is; two names for one list are both.
Why does "is" work for small integers in Python?
CPython creates one object for each integer from -5 to 256 and reuses it, so any two names holding, say, 100 point at the same object. That is an implementation detail, not a language guarantee; larger integers built at run time are usually separate objects. Compare numbers with ==.
Should I use "is None" or "== None"?
Use is None. There is exactly one None object, so identity is the precise test, and it cannot be fooled by a class whose __eq__ returns True for everything. PEP 8 states that comparisons to singletons like None should always use is or is not.
Sources
- Data model: objects, values and types;
__eq__and__hash__— Python Documentation - Expressions: value comparisons and identity comparisons — Python Documentation
- PEP 8: Programming Recommendations — Python Enhancement Proposals
- Integer objects: PyLong_FromLong — Python Documentation
- What's New in Python 3.8: SyntaxWarning for identity checks with literals — Python Documentation