PYTHON / OPERATORS
Identity and membership operators
Tell object identity from value equality with is, and test containment in strings, lists, dicts and sets with in / not in.
What you will learn
- Use is only for singletons: None, True, False, and your own object() sentinels
- Predict when two equal objects are not identical, and when aliasing shares mutations
- Know what in means per container: substring for str, keys for dict, elements for list
- Use x is None for optional arguments so a legitimate 0 or "" survives
Understanding Identity and membership operators
Every name in Python is a reference to an object. The == operator asks the two objects whether they represent the same value, by calling __eq__; the is operator asks a much narrower question, whether the two references point to one and the same object, which is exactly id(x) == id(y). Two lists built separately from the same numbers are equal but not identical, so a is b is False while a == b is True. When is does return True you have an alias, and mutating through either name is visible through the other.
Because identity is about which object exists rather than what it contains, is is only meaningful for objects that exist exactly once. None, True and False are such singletons, which is why x is None is the idiomatic emptiness check, and why a private sentinel = object() is the standard way to detect "argument not supplied" when None is a valid value. Comparing numbers or strings with is sometimes appears to work because CPython caches small integers and interns short identifier-like strings, but that is an optimisation detail of one interpreter, not a promise of the language, and it collapses as soon as the value is computed at runtime.
The membership operators in and not in delegate to the container. If the type defines __contains__, that method decides: str looks for a contiguous substring, dict looks only at its keys, set and dict use hashing so the test costs the same whether the container holds ten or ten million items. If there is no __contains__, Python iterates and compares each element, and that comparison is "identical or equal", which is why an object can be found in a list even when it is not equal to itself. Both operators sit at comparison precedence, so they bind looser than arithmetic and tighter than not, and, or.
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b, a is b)
print(a is c, id(a) == id(c))
a.append(4)
print(b, c)
print(2 in a, 5 in a, 5 not in a)
config = {"host": "localhost", "port": 8080}
print("host" in config, 8080 in config, 8080 in config.values())is compares object identity while == compares value, and in asks the container itself what containment means for its type.
Worked examples
is None as a real default marker
Shows why identity against None is the correct test for an optional argument whose valid values include 0.
def bar(width=None):
if width is None:
width = 80
return "#" * width
print(len(bar()))
print(len(bar(5)))
print(len(bar(0)))
print(bar(3))Example explained
Line 1width=None gives the function one specific object to recognise as "caller said nothing".
Line 2width is None is True only for that singleton, so the default 80 is applied just once.
Line 3bar(0) keeps 0 because 0 is a different object from None; a `if not width` test would wrongly turn it into 80.
Line 4The last call proves the argument is used unchanged when supplied.
in means different things per container
Contrasts substring membership in a string with whole-element membership in a list and key membership in a dict.
line = "10.0.0.1 - GET /index.html"
print("0.0.1" in line)
print("GET" in line)
methods = ["GET", "POST"]
print("GE" in methods)
print("GET" in methods)
print("port" not in {"host": "localhost"})Example explained
Line 1str.__contains__ searches for a contiguous run of characters, so a fragment like "0.0.1" matches.
Line 2"GE" in methods is False because a list compares whole elements with ==, never parts of them.
Line 3"GET" in methods is True since one element is equal to the whole string.
Line 4For a dict, in and not in inspect keys only, so "port" is reported absent even though the dict has values.
Membership checks identity before equality
Demonstrates that list membership succeeds for an object that is not equal to itself, because the identity test runs first.
nan = float("nan")
print(nan == nan)
print(nan in [nan])
print(float("nan") in [float("nan")])Example explained
Line 1IEEE-754 requires nan == nan to be False, so equality alone can never find it.
Line 2nan in [nan] is True because the scan asks "is this the same object?" before asking "is it equal?".
Line 3The last line builds two separate nan objects, so neither identity nor equality holds and the result is False.
Line 4The lesson generalises: x in container answers yes if any element is x or equals x.
Important notes
is has no overloadable behaviour: a class can redefine == via __eq__ and in via __contains__, but nothing can change what is means, which is why is is immune to a class that claims equality with everything.
Membership over an iterator or generator consumes the items it walks, so a second in test on the same generator can return False for an element that was there.
Common mistakes
Using is to compare strings or numbers, as in if name is "admin". It passes for a literal typed in the same file because CPython interns it, then silently fails once the value comes from input() or a slice, so the branch is never taken.
Reading value in some_dict as a search over values. It only tests keys, so 8080 in config is False for {"port": 8080} and validation code wrongly reports the value as missing; you need 8080 in config.values().
Testing a substring when equality was meant, like if "1" in ip_address. Any address containing the digit 1 matches, producing false positives that look like a logic bug far away from the comparison.
Try it yourself
Change, predict, then run
Create two dicts with the same contents, print their == and is results, then bind a third name to the first dict, add a key through that alias, and print both dicts to see which one changed. Finish by printing whether the new key and its value are each in the dict.
Open the Python workspaceCheck your understanding
A function signature is def repeat(times=None). Callers may legitimately pass 0, and when they pass nothing the function should use 10. Which check does the job?
- if times is None: times = 10
- if not times: times = 10
- if times == None: times = 10
- if times is 0: times = 10
Show answer
None is a singleton, so times is None is True for exactly the one case where no argument arrived and leaves a passed 0 untouched. Option 2 is tempting because it looks shorter, but 0 is falsy, so it silently rewrites a deliberate 0 into 10. Option 3 usually behaves the same but routes through __eq__, which an argument's class can define, and option 4 tests identity against a value, which is the wrong question entirely.