is vs == — Identity vs Equality
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?"
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?
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)}")
The Output
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
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
is for value comparison; reserve identity checks strictly for sentinel values like None.is) with object equality (==), especially for common types like strings where optimizations (interning) can sometimes make is behave like ==.