This lesson covers four small but important operators: in, not in, is, and is not.

Membership: in and not in

in asks “is this value inside that collection?”. It works with strings, lists, sets, dictionaries — anything you can iterate over.

# in a string — substring check
text: str = "Hello, Python!"
print("Python" in text)         # True
print("python" in text)         # False — case matters
print("xyz" not in text)        # True

# in a list — item check
fruits: list[str] = ["apple", "banana", "cherry"]
print("apple" in fruits)        # True
print("grape" not in fruits)    # True

# in a dictionary — KEY check
prices: dict[str, float] = {"apple": 0.5, "banana": 0.3}
print("apple" in prices)        # True — apple is a key
print(0.5 in prices)            # False — 0.5 is a value, not a key

not in is the negation. x not in coll is the same as not (x in coll) — the first form reads more naturally.

Why this matters

in is the cleanest way to check if a value is “one of these”. Compare:

status: str = "approved"

# clumsy
if status == "approved" or status == "pending" or status == "in_review":
    ...

# Pythonic
if status in ("approved", "pending", "in_review"):
    ...

The second form is shorter, easier to extend, and reads almost like English.

Identity: is and is not

is asks a different question from ==. We touched on this in the comparison lesson — let’s go deeper.

  • == asks: do these two values look the same?
  • is asks: are these two variables pointing to the same object in memory?
a: list[int] = [1, 2, 3]
b: list[int] = [1, 2, 3]
c: list[int] = a

print(a == b)   # True  — same contents
print(a is b)   # False — different objects in memory
print(a is c)   # True  — c was assigned from a, so they're the same object

Most of the time you want ==. The one place where is is the right answer:

if value is None:
    ...

None is a singleton — there’s only ever one of it in the whole program. So is None is faster and idiomatic.

You’ll see linters (including Ruff) flag == None as a warning and suggest is None instead.

A small mutable vs immutable mystery

Try this in the REPL:

>>> a = 256
>>> b = 256
>>> a is b
True

>>> a = 257
>>> b = 257
>>> a is b
False  # on some Python versions

Why? Python caches small integers (-5 to 256) as singletons for speed. Two variables holding 256 point to the same object. Two variables holding 257 may or may not, depending on the implementation.

The moral: don’t use is to compare numbers, strings, or other values. Use ==. Reserve is for None.

Putting it together

A real-world example using both in and is:

user_role: str | None = get_role()    # may return None

if user_role is None:
    print("Please log in")
elif user_role in ("admin", "moderator"):
    print("Welcome, staff")
else:
    print("Welcome, user")
  • is None checks for the absence of a value.
  • in (...) checks if the value is one of a set of options.

What’s next

You’ve met every operator that produces a boolean. Next, the ternary operator — a one-line if/else for assignments.

Toggle theme (T)