
Python identity operators are a concept that trips up many beginners, especially those coming from other programming languages. When you compare two variables in Python, you have two very different questions you can ask: are these values equal, or are these the exact same object in memory? Python identity operators answer that second question. The is and is not operators check whether two variables refer to the exact same object, not just whether they hold the same value — and understanding that distinction is essential to writing correct Python.
Python provides two identity operators:
is — returns True if both variables point to the same object in memoryis not — returns True if both variables point to different objects in memoryThese are different from equality operators (== and !=). The equality operator checks whether two values are the same. The Python identity operator checks whether two variables are literally the same object — stored at the same memory address. Think of it like asking whether two people have the same name versus whether you are literally looking at the same person.
Here is a quick first look at Python identity operators in action:
a = [1, 2, 3]
b = a
c = [1, 2, 3]
print(a is b)
print(a is c)
print(a == c)
Output
True
False
True
Here b was assigned a directly, so both variables point to the same list object in memory — a is b returns True. The variable c holds a list with identical contents, but it is a completely separate object — so a is c returns False even though a == c is True.
To really understand Python identity operators, you need to understand how Python manages objects in memory. Every object in Python has a unique identity — a numeric ID that represents its memory address. You can inspect this using Python's built-in id() function.
x = "hello"
y = "hello"
z = x
print(id(x))
print(id(y))
print(id(z))
print(x is y)
print(x is z)
Output
140234567890112
140234567890112
140234567890112
True
True
You might notice that x is y returns True here even though x and y were assigned separately. That is because Python applies an optimization called string interning for short strings and string literals. Python caches certain immutable objects and reuses them rather than creating duplicates. So x and y end up pointing to the same string object in memory.
This is exactly why you should not rely on is to compare strings in general — the behavior depends on Python's internal optimizations, which are not guaranteed. Use == to compare values and is only when you genuinely need to check object identity.
a = "hello world this is a longer string"
b = "hello world this is a longer string"
print(a == b)
print(a is b)
Output
True
False
For longer strings that Python does not intern, the two variables are separate objects in memory, so is returns False even though the values are identical.
The single most common and correct use of the is operator in Python is checking for None. Python guarantees that there is exactly one None object in the entire runtime. Every variable set to None points to that same singleton object. Because of this guarantee, the Python community convention is to always use is (not ==) when checking for None.
def find_user(user_id):
if user_id == 0:
return None
return {"id": user_id, "name": "Alice"}
result = find_user(0)
if result is None:
print("No user found")
else:
print("User found:", result)
Output
No user found
Using result is None is both semantically correct and idiomatic Python. It directly expresses the intent: check if result is the None singleton, not just whether it equals some value that happens to be falsy.
You will often see the is not operator used alongside None checks in the same way:
config = {"debug": True}
if config is not None:
print("Config loaded:", config)
Output
Config loaded: {'debug': True}
This pattern is extremely common in Python functions that may or may not return a meaningful value — functions that return None as a sentinel to indicate absence or failure.
Python also applies object caching to small integers, typically in the range of -5 to 256. These integers are pre-allocated when the Python interpreter starts and reused whenever you create a variable with that value. This is called the integer cache or small integer cache.
a = 100
b = 100
print(a is b)
print(id(a), id(b))
x = 1000
y = 1000
print(x is y)
print(id(x), id(y))
Output
True
140234567890944 140234567890944
False
140234601234560 140234604567200
The integers 100 and 100 resolve to the same cached object, so a is b returns True. But 1000 and 1000 are outside the cached range — Python creates two separate integer objects, so x is y returns False. This behavior is an implementation detail of CPython and not something you should depend on in production code. It illustrates why Python identity operators are intended for object identity checks, not value comparisons.
The is not operator is the logical inverse of is. It returns True when two variables point to different objects in memory. It is more readable than writing not (a is b) and is the preferred style in Python.
items = [10, 20, 30]
copy = items[:]
print(items is not copy)
print(items == copy)
reference = items
print(items is not reference)
Output
True
True
False
Here copy was created using a slice, which produces a new list object — even though the contents are identical. So items is not copy returns True. The variable reference is just another name pointing to the same list, so items is not reference returns False.
Understanding when to use is versus == is one of the most important distinctions in Python. Here is a clear breakdown:
The equality operator == calls the __eq__ method on the object. This means the object gets to define what "equal" means. Two different objects can be equal. The Python identity operator is bypasses any method calls and directly compares memory addresses using id(). No custom behavior is possible — it is purely about object identity.
class Box:
def __init__(self, value):
self.value = value
def __eq__(self, other):
return self.value == other.value
box1 = Box(42)
box2 = Box(42)
print(box1 == box2)
print(box1 is box2)
Output
True
False
The two Box objects have the same value, so == returns True because __eq__ says so. But they are two separate objects in memory, so is returns False. This is a perfect illustration of the difference: equality is about value semantics, identity is about object semantics.
A useful debugging technique when learning Python identity operators is to use id() alongside is to verify what is happening in memory. When a is b is True, it is always true that id(a) == id(b) — they are two ways of expressing the same fact.
def show_identity(name_a, obj_a, name_b, obj_b):
print(f"{name_a} id: {id(obj_a)}")
print(f"{name_b} id: {id(obj_b)}")
print(f"{name_a} is {name_b}: {obj_a is obj_b}")
print()
nums = [1, 2, 3]
alias = nums
clone = list(nums)
show_identity("nums", nums, "alias", alias)
show_identity("nums", nums, "clone", clone)
Output
nums id: 140234567892416
alias id: 140234567892416
nums is alias: True
nums id: 140234567892416
clone id: 140234601235584
nums is clone: False
The alias shares the same memory address as nums, confirming they are the same object. The clone has a different address because list() creates a new object with copied contents.
There are clear guidelines in Python for when identity operators are the right choice:
Use is and is not when:
None — this is the most common and idiomatic useUse == and != when:
The Python style guide PEP 8 explicitly recommends using is and is not for None comparisons: "Comparisons to singletons like None should always be done with is or is not, never the equality operators."
This example brings together all aspects of Python identity operators — comparing with None, checking object references, using id(), and understanding the difference from equality operators.
class Config:
def __init__(self, settings):
self.settings = settings
def __eq__(self, other):
if not isinstance(other, Config):
return False
return self.settings == other.settings
def load_config(source):
if source == "empty":
return None
return Config({"theme": "dark", "debug": False})
# None identity check
cfg = load_config("empty")
if cfg is None:
print("No config loaded — using defaults")
cfg = load_config("file")
if cfg is not None:
print("Config loaded successfully")
print("Settings:", cfg.settings)
# Identity vs equality with Config objects
cfg1 = Config({"theme": "dark", "debug": False})
cfg2 = Config({"theme": "dark", "debug": False})
cfg3 = cfg1
print("\nIdentity checks:")
print(f"cfg1 is cfg2: {cfg1 is cfg2}")
print(f"cfg1 is cfg3: {cfg1 is cfg3}")
print("\nEquality checks:")
print(f"cfg1 == cfg2: {cfg1 == cfg2}")
print(f"cfg1 == cfg3: {cfg1 == cfg3}")
print("\nMemory addresses:")
print(f"id(cfg1): {id(cfg1)}")
print(f"id(cfg2): {id(cfg2)}")
print(f"id(cfg3): {id(cfg3)}")
# Integer cache behavior
print("\nInteger cache examples:")
small_a = 50
small_b = 50
large_a = 5000
large_b = 5000
print(f"50 is 50: {small_a is small_b}")
print(f"5000 is 5000: {large_a is large_b}")
# None singleton confirmation
x = None
y = None
print(f"\nNone is None: {x is y}")
print(f"id(x): {id(x)}, id(y): {id(y)}")
Output
No config loaded — using defaults
Config loaded successfully
Settings: {'theme': 'dark', 'debug': False}
Identity checks:
cfg1 is cfg2: False
cfg1 is cfg3: True
Equality checks:
cfg1 == cfg2: True
cfg1 == cfg3: True
Memory addresses:
id(cfg1): 140234567893120
id(cfg2): 140234601236992
id(cfg3): 140234567893120
Integer cache examples:
50 is 50: True
5000 is 5000: False
None is None: True
id(x): 9456832, id(y): 9456832
This example shows every key behavior of Python identity operators in one place: the None singleton check, the difference between identity and equality when __eq__ is defined, how memory addresses confirm what is tells you, and how the integer cache affects identity for small versus large numbers.