The id() built-in function returns the unique integer identity of an object, corresponding to its actual memory address in CPython.
Identity vs. Value Equality: While the equality operator (==) checks whether two objects have matching values, the identity operator (is) evaluates whether two references point to the exact same memory address (id(a) == id(b)). To compute content-based integer hashes for dictionary keys, see hash(). For object type introspection, see type(), or check class hierarchies with isinstance().
The id() function accepts a single object and returns its unique identity integer:
# Formal syntax of built-in id()
memory_identity = id(object)
| Parameter | Type | Default | Description & Operational Rule |
|---|---|---|---|
object |
Any |
Required | The target object whose unique memory identity is retrieved. |
| Return | int |
N/A | A unique integer guaranteed to be constant for the object's entire lifetime. |
| CPython Implementation | Memory Pointer | N/A | Directly reflects the memory address of the C-level PyObject structure. |
In CPython, wrapping id() in hex() reveals the standard hexadecimal memory address:
# 1. Inspecting memory identity and address formatting
user_score = 100
user_alias = user_score
print("id(user_score): ", id(user_score))
print("id(user_alias): ", id(user_alias))
print("Memory address (hex):", hex(id(user_score)))
print("Identity match (is): ", user_score is user_alias)Terminal Output:
id(user_score): 140301581527624
id(user_alias): 140301581527624
Memory address (hex): 0x7f9a12e35a48
Identity match (is): True
To optimize execution speed and reduce memory fragmentation, CPython pre-allocates an array of small integer singletons from -5 through 256 inclusive. Any integer in this range shares the exact same memory address across the entire runtime:
# 2. CPython small integer caching (-5 to 256)
x = 250
y = 250
print("Integers 250 in cache: ", x is y, "| id(x) == id(y):", id(x) == id(y))
# Numbers outside the cache allocate distinct heap objects
val1 = 1000
val2 = int("1000")
print("Integers 1000 outside: ", val1 is val2, "| val1 == val2:", val1 == val2)
print("Distinct memory addresses:", id(val1) != id(val2))Terminal Output:
Integers 250 in cache: True | id(x) == id(y): True
Integers 1000 outside: False | val1 == val2: True
Distinct memory addresses: True
For mutable objects like lists, in-place element changes do not change the memory identity. For immutable types like tuples and strings, any change allocates a new object:
# 3. Mutable vs. immutable object identity under modification
# Mutable sequence (list): identity is preserved during in-place changes
nums = [1, 2, 3]
initial_list_id = id(nums)
nums.append(4)
nums[0] = 99
print("List ID unchanged: ", id(nums) == initial_list_id)
# Immutable sequence (tuple): modification creates a new object
coords = (10, 20)
initial_tuple_id = id(coords)
coords += (30, 40)
print("Tuple ID changed: ", id(coords) != initial_tuple_id)Terminal Output:
List ID unchanged: True
Tuple ID changed: True
Understanding the distinction between checking identity and comparing content values:
# 4. Identity comparison (is) vs. Equality comparison (==)
list_a = [10, 20, 30]
list_b = [10, 20, 30]
print("Value equality (list_a == list_b): ", list_a == list_b)
print("Identity match (list_a is list_b): ", list_a is list_b)
print("Memory IDs differ: ", id(list_a) != id(list_b))
# Aliasing points to the exact same memory address
list_c = list_a
print("Aliased identity (list_a is list_c): ", list_a is list_c)Terminal Output:
Value equality (list_a == list_b): True
Identity match (list_a is list_b): False
Memory IDs differ: True
Aliased identity (list_a is list_c): True
Object identity is only guaranteed unique among simultaneously existing objects. Once an object is garbage-collected, its memory address may be reassigned to a newly created object:
# 5. Non-overlapping lifetime and memory address recycling
def generate_temporary():
temp = [1, 2, 3]
return id(temp)
first_id = generate_temporary()
second_id = generate_temporary()
print("Address first temp: ", hex(first_id))
print("Address second temp: ", hex(second_id))
print("Memory address reused:", first_id == second_id)Terminal Output:
Address first temp: 0x7f9a12c41980
Address second temp: 0x7f9a12c41980
Memory address reused: True
When traversing complex cyclic data structures or object graphs, tracking visited memory IDs avoids infinite recursion loops:
# 6. Production Recipe: Graph Cycle Detector using Memory Identity
class GraphNode:
def __init__(self, value):
self.value = value
self.neighbors = []
def has_cycle(start_node: GraphNode) -> bool:
"""Detects cycles in an arbitrary object graph by tracking visited memory IDs."""
visited_ids = set()
stack = [start_node]
while stack:
current = stack.pop()
node_id = id(current)
if node_id in visited_ids:
return True # Cycle detected
visited_ids.add(node_id)
for neighbor in current.neighbors:
stack.append(neighbor)
return False
n1 = GraphNode("Root")
n2 = GraphNode("Child_A")
n3 = GraphNode("Child_B")
n1.neighbors.append(n2)
n2.neighbors.append(n3)
print("Acyclic graph has cycle? ", has_cycle(n1))
# Create circular loop (n3 -> n1)
n3.neighbors.append(n1)
print("Circular graph has cycle?", has_cycle(n1))Terminal Output:
Acyclic graph has cycle? False
Circular graph has cycle? True
Never use is to compare numbers or string values. Always reserve is for comparing singletons like None:
# 7. Common traps: using 'is' with literal constants
# WARNING: Comparing non-singletons with 'is' triggers warnings and bugs
number = 1000
# if number is 1000: <-- DON'T DO THIS! Use == instead
print("Correct equality check: ", number == 1000)
# Singletons (None, True, False) MUST be compared with 'is'
empty_state = None
print("Correct singleton check (is):", empty_state is None)Terminal Output:
Correct equality check: True
Correct singleton check (is): True
Practice memory inspection, integer caching boundaries, and cycle detection in our interactive companion Jupyter Notebook:
Explore interactive code cells, benchmark memory allocation, inspect small integer caching, and run self-validating assertions.
View on GitHub Run in Google ColabRun these unit checks directly in Python or your local environment to verify id() mechanics:
# 8. Self-verifying unit test suite for id()
# Test 1: Identity returns integer
assert isinstance(id(object()), int)
# Test 2: Singleton identity guarantee
assert id(None) == id(None)
assert (None is None) is True
# Test 3: Small integer cache identity (-5 to 256)
a = 200
b = 200
assert a is b
assert id(a) == id(b)
# Test 4: Distinct mutable containers have distinct IDs
l1 = [1, 2]
l2 = [1, 2]
assert l1 == l2
assert l1 is not l2
assert id(l1) != id(l2)
# Test 5: In-place mutation maintains list identity
orig_id = id(l1)
l1.append(3)
assert id(l1) == orig_id
print("All id() unit assertions successfully verified!")Terminal Output:
All id() unit assertions successfully verified!
In CPython, id() returns the actual memory address where the object structure is stored in RAM.
CPython pre-allocates an internal cache of singleton integer objects from -5 through 256 for execution speed.
Use is only when comparing object identity against known singletons like None, True, or False.
Yes, but only if their lifetimes do not overlap. If the first object is destroyed, its memory location can be reused by a new object.
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.