← Python Code Memory & Data Structures
Browse Python Concepts

is vs == — Identity vs Equality

Mental Model

Think of is as asking, "Are these two labels pointing to the exact same physical object in memory?" while == asks, "Do the contents of the objects these labels point to represent the same value?"

Rule: Never use is for value comparison; reserve identity checks strictly for sentinel values like None.

The Setup

You are parsing API keys and incoming message IDs in an asynchronous messaging queue. You write an authorization filter that checks if the incoming consumer token matches the local system credentials.

What Does This Print?

Broken code
Python
def check_token(received_token, system_token):
    # Identity check for fast rejection
    if received_token is system_token:
        return True
    return False

# Test credentials
sys_tok = "PROD_API_KEY_9999"
user_tok = "".join(["PROD_API_KEY_", "9999"])

print(f"Value equals: {sys_tok == user_tok}")
print(f"Identity is: {sys_tok is user_tok}")
print(f"Authorized: {check_token(user_tok, sys_tok)}")
Predict if the identity check passes when comparing the dynamic string to the hard-coded API key.

The Output

What actually happens
Value equals: True Identity is: False Authorized: False

The values are identical, but the is check returns False and rejects the user. The dynamic string creation bypassed Python's compiler-level optimizations, resulting in two separate string objects residing at distinct addresses in memory.

Why Python Does This

The == operator evaluates value equality by invoking the target's __eq__ method, checking if both objects represent the same data. The is operator checks identity by comparing the memory addresses (id()) of the two variables directly. While Python interns certain objects like small integers (-5 to 256) and basic identifier-like strings for memory optimization, dynamically constructed strings or values outside those ranges are allocated as unique PyObject addresses on the heap.

The Fix

Corrected pattern
Python
def check_token(received_token, system_token):
    # Always use comparison operators for value checks
    return received_token == system_token

sys_tok = "PROD_API_KEY_9999"
user_tok = "".join(["PROD_API_KEY_", "9999"])
# Now correctly handles matching values across separate memory instances

Using == explicitly invokes the __eq__ method (or its default implementation) which compares the values or contents of the objects. This is the correct semantic comparison when you want to know if two objects represent the same data, regardless of their memory location.

How This Fails in Real Systems

A payment gateway check for transaction IDs between $10 and $1000 worked perfectly in testing, but failed in production for amounts over $256. The developer had written amount is expected_amount, which worked for small interned integers in local sandboxes but failed for larger numbers.

Key Takeaway

Never use is for value comparison; reserve identity checks strictly for sentinel values like None.
Common mistake: Developers confuse object identity (is) with object equality (==), especially for common types like strings where optimizations (interning) can sometimes make is behave like ==.