Identity vs Equality, and Membership
Know exactly when to use `is` versus `==`, and how `in` really works.
Two of the most misunderstood operators in Python are is and ==. They answer different questions:
==asks "are these two values equal?" — it calls the object's__eq__.isasks "are these the exact same object in memory?" — it compares identity (thinkid(x)).
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True — same contents
print(a is b) # False — two different list objects
print(a is a) # True — literally the same objectThe one rule that never fails
Use is only for singletons: None, True, False. Write if x is None, never if x == None. For everything else — numbers, strings, lists — compare with ==.
The small-int trap
CPython caches small integers (-5 to 256) and short strings, so 256 is 256 may be True while 257 is 257 may be False. This is an implementation detail — never rely on it.
Membership with in
in tests membership and works on any container. For a list or str it scans (O(n)); for a set or dict it uses hashing (O(1)). Choosing the right container turns a slow lookup into an instant one.
Example
# A private sentinel: distinguishes 'no argument given' from 'given None'
_MISSING = object()
def get(config, key, default=_MISSING):
if key in config: # membership: O(1) on a dict
return config[key]
if default is _MISSING: # identity: is this OUR sentinel?
raise KeyError(key)
return default
cfg = {'host': 'localhost', 'port': None}
print(get(cfg, 'host')) # localhost
print(get(cfg, 'port')) # None — the key exists, value is None
print(get(cfg, 'timeout', 30)) # 30 — key absent, fall back
try:
get(cfg, 'missing')
except KeyError as e:
print('raised KeyError:', e)
# Why the sentinel? default=None could not tell these two apart:
print('port present?', 'port' in cfg) # True even though value is None
# Output:
# localhost
# None
# 30
# raised KeyError: 'missing'
# port present? TrueWhen to use it
- A sentinel check uses 'is None' to detect missing optional arguments instead of '== None'.
- A membership guard uses 'key in config_dict' to safely check for a key before accessing it.
- An interning test uses 'is' on small integers to explain Python's caching behaviour to a junior developer.
More examples
is vs == for None
Shows why 'is None' is preferred: it cannot be tricked by a class overriding __eq__.
value = None
print(value is None) # True (identity check)
print(value == None) # True (but fragile — __eq__ can be overridden)
class Weird:
def __eq__(self, other): return True
w = Weird()
print(w == None) # True (wrong!)
print(w is None) # False (correct)Membership with 'in'
Uses 'in' on a set and a dict; for sets it is O(1), for dicts it checks keys only.
roles = {'admin', 'editor', 'viewer'}
print('admin' in roles) # True (set: O(1))
print('guest' not in roles) # True
config = {'debug': True, 'port': 8080}
print('port' in config) # True (dict checks keys)Integer interning gotcha
Demonstrates CPython's small-integer cache and why 'is' must never be used for value equality.
a = 256
b = 256
print(a is b) # True (CPython caches -5..256)
c = 257
d = 257
print(c is d) # False in some contexts (not cached)
print(c == d) # True — always use == for value equality
Discussion