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__.
  • is asks "are these the exact same object in memory?" — it compares identity (think id(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 object

The 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

Example · python
# 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? True

When 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__.

Example · python
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.

Example · python
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.

Example · python
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

  • Be the first to comment on this lesson.