Back to Blog
Python

Python Object Identity: How `is` and `id()` Work

python object identity: Learn how Python object identity works, how `id()` and `is` behave, and when to use identity checks instead of equality.

object identityis operatorid()python internalsequality
Diagram showing two Python objects with different identity but equal values, illustrating `is` vs `==`.

In Python, every object has a unique identity that exists independently of its value. Understanding python object identity is essential for writing correct code when you compare objects, cache results, or rely on default behavior of containers. This article explains how identity works, how id() and is expose it, and where identity checks are the right tool.

What id() Actually Returns

The built-in id() function returns an integer that is guaranteed to be unique among simultaneously existing objects. In CPython, this integer is the memory address of the object, but the language specification only guarantees that it is a unique identifier for the object's lifetime. The value can change between runs, so you should never persist it or treat it as a stable hash.

a = [1, 2, 3] b = [1, 2, 3] print(id(a)) # e.g., 140124567890 print(id(b)) # e.g., 140124567912

Because a and b are distinct list objects, they have different identities. The id() values differ even though the lists compare equal. This distinction is the core of object identity.

The is Operator: Comparing Identity

The is operator checks whether two names refer to the exact same object. It is equivalent to comparing id() values but is more readable and faster because it directly compares pointers under the hood.

a = [1, 2, 3] b = [1, 2, 3] c = a print(a is b) # False print(a is c) # True

Here a is c is True because c is just another reference to the same list. a is b is False because the two lists are separate objects with identical contents.

Use is when you need to know whether two names point to the same object, not when you care about value equality. For value comparison, use ==.

Why Small Integers Are Reused

In CPython, integers from -5 to 256 are cached and reused. This means that any reference to the integer 5 points to the same object, so is comparisons often return True for these values.

a = 5 b = 5 print(a is b) # True (CPython)

This behavior is an implementation detail. It is not guaranteed by the Python language specification. Relying on it for identity checks is fragile because other Python implementations or future versions may not cache the same range. For integers outside the cache, is typically returns False even for equal values.

a = 257 b = 257 print(a is b) # False in CPython, but not guaranteed

The practical lesson is to never use is for integer comparison. Always use == unless you specifically need to know whether two variables reference the same object.

String Interning and Identity

Similar to integers, Python interns some strings—usually short identifiers and literals that look like identifiers. This means that two string literals with the same content may point to the same object.

s1 = "hello" s2 = "hello" print(s1 is s2) # True in CPython for short strings

But this is not reliable for strings created dynamically, such as those from user input or concatenation:

s3 = "hello" s4 = "" .join(["h", "e", "l", "l", "o"]) print(s3 is s4) # False in CPython

String interning is a memory optimization, not a feature you should depend on for correctness. Use == for string value comparison. The only time is is appropriate for strings is when you are comparing against a known singleton like None.

Identity and Mutable Objects

For mutable objects like lists, dicts, and sets, identity is straightforward: each call to a constructor creates a new object, even if the contents are identical. This matters when you pass objects to functions or store them in data structures.

def append_to_list(lst, item): lst.append(item) a = [] b = [] append_to_list(a, 1) print(a) # [1] print(b) # []

Here a and b are distinct objects, so mutating a does not affect b. If you want to share state, you need to pass the same object explicitly.

Identity also affects how default arguments behave. A common mistake is using a mutable default argument, which creates a single object shared across all calls:

def add_item(item, lst=[]): lst.append(item) return lst print(add_item(1)) # [1] print(add_item(2)) # [1, 2]

The default list is created once and reused because it is the same object every time. Use None as a sentinel and create a new list inside the function to avoid this behavior.

When is Is the Right Choice

The most common correct use of is is comparing against singletons like None, True, and False. These are guaranteed to be unique objects, so identity checks are both safe and slightly faster than ==.

if value is None: # handle missing value

Another use case is checking whether two variables reference the same object for caching or memoization. For example, if you have a function that returns a canonical object, you can verify the result is the same instance:

def get_canonical(obj): # returns a shared instance return _registry.get(obj, obj) result = get_canonical(some_obj) if result is some_obj: print("No new object created")

You can also use is to implement sentinel values. Define a unique sentinel object and compare with is to detect whether a parameter was provided or a default was used.

Common Pitfalls with is vs ==

Mixing up is and == leads to subtle bugs, especially with immutable types. For example, comparing two numbers with is may work in a REPL session but fail in a script or after arithmetic operations.

def check(x): return x is 1000 print(check(1000)) # False in CPython

Even if a value is cached, relying on that is dangerous. The same applies to strings built at runtime. The rule is simple: use == for value equality unless you have a specific reason to check identity.

Another pitfall is using is with floats. Floating-point literals are not interned, so 0.1 is 0.1 may be True in the same expression but False when assigned to variables due to constant folding. Never use is for numeric comparison.

Performance and Runtime Considerations

Identity checks are generally faster than equality checks because they only compare memory addresses, while == may invoke arbitrary comparison logic. However, the difference is tiny and rarely matters unless you are in a tight loop. The bigger risk is using is incorrectly, which can cause logic errors that are hard to debug.

If you are optimizing code that compares many objects, first profile to see if equality comparison is a bottleneck. In most cases, the overhead of == is negligible compared to the cost of building the objects themselves.

One performance-related use of identity is in caching and memoization. Storing objects in a dictionary keyed by their identity (via id()) is possible but dangerous because id() can be reused after an object is garbage collected. A safer approach is to use a WeakKeyDictionary from the weakref module, which keys by object identity without preventing garbage collection.

import weakref cache = weakref.WeakKeyDictionary() obj = SomeClass() cache[obj] = "data"

This uses identity semantics internally but handles object lifetime correctly. It is a better choice than manually managing id() values.

Memory and Caching Implications

Object identity is closely tied to memory management. When two objects are equal but distinct, they consume separate memory. Interning and caching reduce memory usage for frequently used immutable values, but they also create the illusion that identity and equality are the same. Understanding which objects are interned helps you predict memory behavior, but you should not rely on it for correctness.

For mutable objects, identity is the only reliable way to track whether two references point to the same underlying data. This is crucial when implementing algorithms that depend on object identity, such as graph traversal or reference counting.

In practice, the most important takeaway is to choose the comparison operator based on the semantic question you are asking. If you ask "are these the same object?", use is. If you ask "do these objects have the same value?", use ==. Keeping this distinction clear in your code prevents a whole class of bugs and makes your intentions explicit for other developers reading your code.

python object identity: Practical Usage and Code Examples | RYUSLOG DEV