id() Function Complexity¶
The id() function returns an integer representing an object's identity. In
CPython, this is the object's memory address. The identity is guaranteed to be
unique and constant for an object during its lifetime.
Complexity Reference¶
| Operation | Time | Space | Notes |
|---|---|---|---|
id(obj) |
O(1) | O(1) | Returns object identity as an integer; memory address in CPython |
Object Identity and Lifetime¶
Object identities are guaranteed to be unique only among simultaneously existing objects.
Coexisting Objects¶
When two objects exist at the same time, their IDs are guaranteed to differ unless they are the same object.
# O(1) - distinct objects existing concurrently have distinct IDs
a = [1, 2, 3]
b = [1, 2, 3]
print(id(a) == id(b)) # False - different objects in memory
# O(1) - aliases share the same identity
c = a
print(id(a) == id(c)) # True - same object
Address Recycling Across Non-Overlapping Lifetimes¶
When an object is deallocated, its memory address may be immediately reused by a newly allocated object. Consequently, comparing IDs of objects with non-overlapping lifetimes can produce identical values for distinct objects.
# O(1) - the first list is destroyed before the second is created; CPython reuses the address
print(id([]) == id([])) # True - address recycled after deallocation
# O(1) - both lists exist concurrently, so their identities differ
x, y = [], []
print(id(x) == id(y)) # False - non-overlapping lifetimes are required for reuse
Singletons and Cached Values¶
CPython caches small integers (-5 to 256), interned strings, and built-in
singletons like None, True, and False. References to these values share
the same object identity.
# O(1) - cached small integers share identity
x = 42
y = 42
print(id(x) == id(y)) # True - same cached int object
# O(1) - None is a singleton
print(id(None) == id(None)) # True
Identity Testing: is Operator vs id()¶
To test whether two references point to the same object, use the is operator
instead of comparing id() results.
a = [1, 2, 3]
b = a
c = [1, 2, 3]
# is operator: direct pointer comparison - O(1) time, 0 extra bytes
print(a is b) # True
print(a is c) # False
# id() comparison: calls id() twice, allocates integers, and runs int.__eq__ - O(1)
print(id(a) == id(b)) # True
The is operator has two key advantages:
- Efficiency:
a is bcompares pointers directly at the bytecode level without function call overhead or integer object allocation. - Safety:
isrequires both objects to exist concurrently. Unlikeid(a) == id(b),iscan never returnTruefor distinct objects through address recycling ([] is []is alwaysFalse).
Common Use Cases¶
Cycle Detection in Traversals¶
When traversing recursive or cyclic data structures, id() can track visited
nodes. Using id(obj) in a visited set avoids TypeError: unhashable type on
mutable containers (dict, list, set) and avoids triggering recursive
equality checks.
A cycle exists when an edge leads back to an active ancestor. Completed nodes can be shared without forming a cycle and need not be traversed again. This example follows dictionary values and list, tuple, and set elements. The root keeps reachable nodes alive; the containers must remain unchanged during traversal.
# O(V + E) time, O(V) space to detect reference cycles across V nodes and E edges
def has_cycle(obj):
active = set()
completed = set()
def visit(node):
node_id = id(node) # O(1)
if node_id in active:
return True
if node_id in completed:
return False
active.add(node_id) # O(1) average
if isinstance(node, dict):
children = node.values()
elif isinstance(node, (list, tuple, set)):
children = node
else:
children = ()
for child in children:
if visit(child):
return True
active.remove(node_id)
completed.add(node_id)
return False
return visit(obj)
# Self-referential structure
node = {}
node["child"] = node
print(has_cycle(node)) # True - cycle detected
# Acyclic structure
tree = {"left": [1, 2], "right": [3, 4]}
print(has_cycle(tree)) # False
# Shared references are not cycles
shared = [1, 1]
print(has_cycle([shared, shared])) # False
Identity Caching and Weak References¶
Using id(obj) as a dictionary key to associate metadata with an object is an
anti-pattern if obj is not held alive. Once obj is garbage collected, a
subsequent object allocated at the same address will collide with the stale key.
# O(1) - storing raw IDs without keeping the referenced object alive can collide
cache = {}
cache[id(list(range(10)))] = "metadata" # Object immediately freed; key may collide
# O(1) - weakref.WeakKeyDictionary removes entries when keys are garbage collected
import weakref
class Tracked:
pass
registry = weakref.WeakKeyDictionary()
item = Tracked()
registry[item] = "metadata"
WeakKeyDictionary uses keys' hashing and equality, not identity. For per-object
metadata, use weak-referenceable keys with identity-based equality and hashing,
as in Tracked above. Distinct equal keys share an entry; that entry disappears
when the originally inserted key is collected, even if another equal key remains alive.
Best Practices¶
✅ Do:
- Use the
isoperator (x is None,x is y) for identity checks. - Use
id()when hashing unhashable objects during active graph traversals where objects remain alive. - Use
weakref.WeakKeyDictionaryfor per-object metadata only with weak-referenceable keys that use identity-based equality and hashing.
❌ Avoid:
- Comparing
id(a) == id(b)whena is bis intended. - Using
id(obj)as a dictionary key without ensuringobjstays alive. - Persisting
id()values across processes or Python sessions.
Related Functions¶
- hash() - Returns an object's hash value for dictionary and set lookups.
- isinstance() - Type checking against a class or tuple of classes.
- type() - Retrieves the runtime type of an object.