Identity, Equality, and Mutability
Identity, Equality, and Mutability
After reading this chapter, you will master the fundamental distinction between identity (is) and equality (==), know how CPython caches small integers and strings, and avoid shared mutable state bugs.
Mental model
In Python, every object has three core attributes: 1. Identity: The memory address where the object resides (id(obj)). 2. Type: What kind of object it is (e.g. int, str, list), which never changes. 3. Value: The data stored in the object.
Equality (==) compares VALUES
┌─────────────────────────────────────┐
│ "production" "production"│
└─────────────────────────────────────┘
▲ ▲
│ │
┌───────────────┐ ┌───────────────┐
│ Object A │ │ Object B │
│ id: 0x104a │ │ id: 0x208f │
└───────────────┘ └───────────────┘
└─────────────────────────────────────┘
Identity (is) compares MEMORY IDs
(Are they the exact same object?)
a == b: Evaluatesa.__eq__(b). Asks: Do these two objects have equivalent contents?a is b: Evaluatesid(a) == id(b). Asks: Are these two variables pointing to the exact same memory address?
Minimal example
Save this file as identity_demo.py:
# identity_demo.py
def main() -> None:
# Two distinct list objects with identical contents
list_a = [10, 20, 30]
list_b = [10, 20, 30]
print(f"list_a == list_b : {list_a == list_b} (Values match)")
print(f"list_a is list_b : {list_a is list_b} (Different objects in memory)")
print(f"id(list_a) : {hex(id(list_a))}")
print(f"id(list_b) : {hex(id(list_b))}")
# The singleton None must ALWAYS be checked with 'is'
status = None
print(f"status is None : {status is None}")
if __name__ == "__main__":
main()Run via uv run python identity_demo.py:
list_a == list_b : True (Values match)
list_a is list_b : False (Different objects in memory)
id(list_a) : 0x7f5110a0
id(list_b) : 0x7f511120
status is None : True
Worked examples
Case 1: The small integer caching mechanism
CPython pre-allocates and caches small integers in the range [-5, 256] during startup. Any reference to an integer in this range shares the pre-allocated singleton instance.
# int_cache.py
def test_caching() -> None:
# Inside the cache range [-5, 256]
x = 250
y = 250
print(f"250 is 250: {x is y} (Shared singleton from CPython cache)")
# Outside the cache range (typically > 256)
big1 = 1000
big2 = 1000
print(f"1000 is 1000: {big1 is big2} (Distinct heap allocations)")
print(f"1000 == 1000: {big1 == big2} (Values are still equal)")
if __name__ == "__main__":
test_caching()Run:
uv run python int_cache.pyOutput:
250 is 250: True (Shared singleton from CPython cache)
1000 is 1000: False (Distinct heap allocations)
1000 == 1000: True (Values are still equal)
Why: Never rely on is for integer or string equality. Always use == for values.
Case 2: Shallow copy vs Deep copy
When copying nested mutable collections, a shallow copy copies references to inner objects, while a deep copy recursively duplicates everything.
# copy_semantics.py
import copy
def main() -> None:
original = {"cluster": "prod", "nodes": ["node-1", "node-2"]}
# Shallow copy
shallow = original.copy()
# Deep copy
deep = copy.deepcopy(original)
# Mutate the nested list
original["nodes"].append("node-3")
print(f"Original nodes: {original['nodes']}")
print(f"Shallow nodes : {shallow['nodes']} (Mutated because inner list reference was shared)")
print(f"Deep nodes : {deep['nodes']} (Isolated because inner list was cloned)")
if __name__ == "__main__":
main()Run:
uv run python copy_semantics.pyPitfalls
Pitfall 1: Comparing values using is
# Bug: Works sometimes due to caching, fails unexpectedly in production!
if user_input is "admin": # WRONG! Raises SyntaxWarning in modern Python
pass
# Correct:
if user_input == "admin": # Correct value equality check
passPitfall 2: Mutating objects inside tuples
A tuple is immutable, meaning its reference slots cannot be changed. However, if a slot points to a mutable object (like a list), that list can still be mutated in place!
t = (1, [2, 3])
t[1].append(4) # Perfectly valid: the list inside the tuple is modified!
print(t) # (1, [2, 3, 4])Exercises
- Write a script comparing two identical strings constructed at runtime (
s1 = "hello_world"vss2 = "".join(["hello", "_", "world"])). Compare them withisand with==. - Inspect
id(None)across multiple variables set toNone. Verify they all resolve to the exact same pointer address. - Construct a dictionary with a nested dictionary. Perform both a shallow copy and a
copy.deepcopy(). Demonstrate modifying a nested value in the original and check both copies.
Further reading
- CPython Internal Source:
Objects/longobject.c(small integer cache definition). - Python Documentation:
copymodule (Shallow and deep copy operations). - PEP 8: Programming Recommendations (Comparisons to singletons like None).