Python actually gives you three distinct ways to compare things, and they each answer a genuinely different question. == asks "do these have the same value?" The python identity operator, is, asks "are these literally the same object in memory?" And the python membership operator, in, asks "does this value exist somewhere inside this collection?" This article covers the last two — identity and membership — including the mix-up between is and == that catches even developers with some experience.
Two Different Kinds of Comparison
To set the stage, here's the distinction in one line each:
==— compares values. Are these two things equal?is— compares identity. Are these two names pointing to the exact same object?in— checks membership. Does this value exist somewhere inside this collection?
The rest of this article walks through in/not in first, then is/is not, and finishes with the specific mistake that trips people up when they confuse identity with equality.
Membership Operators: in and not in
in
in checks whether a value exists within a sequence — a list, tuple, string, set, or dictionary.
fruits = ["apple", "banana", "cherry"]
print("banana" in fruits) # True
print("grape" in fruits) # Falsenot in
not in is the inverse — it checks that a value does not exist within a collection:
print("grape" not in fruits) # TrueAcross different collection types
Membership checks work consistently across Python's various sequence and collection types:
# Strings — checks for a substring
sentence = "the quick brown fox"
print("quick" in sentence) # True
# Tuples
coordinates = (1, 2, 3)
print(2 in coordinates) # True
# Sets
unique_ids = {101, 102, 103}
print(104 in unique_ids) # FalseA note on dictionaries
Checking in on a dictionary checks its keys by default, not its values:
user = {"name": "Alex", "age": 30}
print("name" in user) # True — 'name' is a key
print("Alex" in user) # False — 'Alex' is a value, not a keyIf you specifically need to check values, you'd check against .values() instead: "Alex" in user.values().
Identity Operators: is and is not
is
is checks whether two variables reference the exact same object in memory — not merely equal values, but the literal same object.
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True — same values
print(a is b) # False — two separate objects, even though they look identicala and b are two independently created lists that happen to contain the same values. They're equal, but they are not the same object.
is not
is not is the inverse:
print(a is not b) # True — confirms they're different objectsUsing id() to see what's really happening
Python's built-in id() function returns a unique identifier representing an object's location in memory. It's a useful way to see the identity distinction directly, rather than taking it on faith:
a = [1, 2, 3]
b = [1, 2, 3]
print(id(a)) # some number, e.g. 140234881672384
print(id(b)) # a different number
print(id(a) == id(b)) # False — confirms they're different objectsIf, instead, you assign one variable to another directly, both names point to the same object, and is correctly reflects that:
a = [1, 2, 3]
c = a # c now refers to the same list object as a
print(a is c) # True
print(id(a) == id(c)) # Trueis vs. ==: Avoiding a Common Mistake
The core distinction, restated
== compares values. is compares identity. These usually agree for immutable values you'd expect to be equal, and usually diverge for separately created mutable objects — which is exactly what makes the mistake so easy to make without noticing.
Why is can appear to "work" — and why that's unreliable
Here's where things get genuinely confusing. For small integers and short strings, Python's implementation (CPython) reuses cached objects for performance reasons, which can make is appear to behave like == in specific cases:
a = 5
b = 5
print(a is b) # True — small integers are cached and reused by CPython
x = "hello"
y = "hello"
print(x is y) # True in most cases — short strings are often internedThis looks like is is working fine as a substitute for == — until it isn't:
a = 500
b = 500
print(a is b) # Often False — outside CPython's small-integer cache rangeThis behavior is a CPython implementation detail, not a guaranteed language feature. It can differ across Python versions, other Python implementations, or even different contexts within the same program. Relying on it is a bug waiting to surface later, on a different system or a slightly different value.
Best practice
Use is/is not only for singleton checks — most importantly, checking against None:
if value is None:
print("No value provided")Use == for everything else — numbers, strings, lists, dictionaries, custom objects, all of it. This isn't a stylistic preference; it's the distinction that keeps your code behaving predictably regardless of which Python implementation or version it happens to run on.
Practical Use Cases and Best Practices
Membership operators in practice
in and not in show up constantly in everyday validation and searching:
allowed_roles = ["admin", "editor", "viewer"]
user_role = "editor"
if user_role in allowed_roles:
print("Access permitted")
else:
print("Access denied")They're just as useful for filtering data — checking whether an item should be included based on membership in some reference collection:
blocked_words = {"spam", "scam", "phishing"}
message = "this looks like spam"
c any(word in message for word in blocked_words)
print(contains_blocked_word) # TrueIdentity operators in practice
Beyond None checks, is is genuinely useful for confirming whether two variable names refer to the same mutable object — which matters for catching a specific category of bug: aliasing, where modifying one variable unexpectedly affects another because they secretly point to the same underlying object.
original = [1, 2, 3]
alias = original # this does NOT create a copy
copy = original[:] # this DOES create a copy
alias.append(4)
print(original) # [1, 2, 3, 4] — changed, because alias points to the same list
print(original is alias) # True — confirms the shared reference
copy.append(5)
print(original) # [1, 2, 3, 4] — unchanged, because copy is a separate object
print(original is copy) # False — confirms they're different objectsUnderstanding is here isn't just academic — it's the tool that lets you diagnose exactly why one part of your program appears to be "mysteriously" affecting another part that looks unrelated.
Readability tip
When combining membership and identity checks in the same condition, parentheses make the intent much clearer:
if (value in allowed_roles) and (other is not None):
print("Both conditions satisfied")Neither operator has particularly surprising precedence on its own, but grouping each check explicitly removes any ambiguity for whoever reads the code next — including you, coming back to it later.