Python Membership Operators

Final Exam & Certification

Complete this tutorial and pass the 10-question final exam to earn a downloadable certificate of completion.

skip to exam

Python gives you two membership operators, in and not in, that let you ask a single plain-English question: is this value inside that container? Understanding them is fundamental to writing clean, readable Python code.

What's in this Python Tutorial

Membership operators are a category of Python operators that evaluate whether one object is a member of another. They always return a Boolean — either True or False. Python provides exactly two: in and not in. Both work across all of Python's built-in sequence and collection types, and they are one of the features that make Python code feel close to natural language.

What Are Membership Operators?

A membership operator tests whether an element exists within a sequence or collection. The left operand is the value being searched for; the right operand is the container being searched.

Note

Membership operators are distinct from identity operators (is / is not). Identity checks whether two names point to the same object in memory. Membership checks whether a value exists inside a container.

The two membership operators and their basic behavior:

python
# in — returns True if the value is found in the container
3 in [1, 2, 3]        # True
7 in [1, 2, 3]        # False

# not in — returns True if the value is NOT found in the container
7 not in [1, 2, 3]    # True
3 not in [1, 2, 3]    # False

Because they return Booleans, membership expressions work directly in if statements and while loops without any comparison to True or False needed.

python
role = "admin"
allowed_roles = {"admin", "editor", "viewer"}  # set: O(1) lookup

if role in allowed_roles:
    print("Access granted")
else:
    print("Access denied")
Pro Tip

Avoid writing if (x in my_list) == True:. The parentheses and comparison are unnecessary — if x in my_list: is already a complete, idiomatic Boolean test.

code builder click a token to place it

Build a membership test that checks whether "python" is in the list called languages:

your code will appear here...
in not in "python" languages is contains
Why: The correct syntax is "python" in languages. The value being searched goes on the left, the container on the right. not in would test the opposite, is tests identity (not membership), and contains is not a Python operator.

Membership Operators Across Container Types

Both in and not in work with every standard Python container, but the semantics differ slightly depending on the type. The accordion below covers each case.

What in checks
Whether the left string appears as a substring anywhere inside the right string. This is a full substring search, not just a single character check.
Example
"py" in "python"True  |  "Py" in "python"False (case-sensitive)
Performance
O(n·m) in the worst case, where n is the string length and m is the substring length. CPython uses optimised algorithms that are fast in practice.
What in checks
Whether any element in the sequence equals the left operand. Python walks the sequence from the first element until a match is found or the end is reached.
Example
2 in [1, 2, 3]True  |  (1, 2) in [(1, 2), (3, 4)]True
Performance
O(n) — linear scan. For frequent membership testing on large collections, prefer a set.
What in checks
Whether the left operand is a member of the set. Internally Python hashes the value and checks the hash table directly.
Example
"admin" in {"admin", "editor"}True
Performance
O(1) average. The fastest option for membership testing when order does not matter and duplicates are not needed.
What in checks
Only the keys by default. To test values, use in dict.values(). To test key-value pairs, use in dict.items().
Example
"name" in {"name": "Alice", "age": 30}True  |  "Alice" in {"name": "Alice"}.values()True
Performance
O(1) average for key lookups (hash table). Value and item lookups are O(n).
python
# String — substring check (case-sensitive)
print("error" in "an error occurred")  # True
print("Error" in "an error occurred")  # False

# List — element equality
colors = ["red", "green", "blue"]
print("green" in colors)               # True
print("purple" not in colors)          # True

# Set — O(1) hash lookup
allowed = {"read", "write", "execute"}
print("delete" not in allowed)         # True

# Dictionary — keys only by default
config = {"host": "localhost", "port": 5432}
print("host" in config)                # True
print("localhost" in config)           # False — checks keys, not values
print("localhost" in config.values())  # True

Practical Patterns and Deeper Techniques

Membership operators appear throughout real Python programs. The examples below go beyond the basics — covering performance trade-offs, protocol-level behavior, and idioms that separate workmanlike Python from well-considered Python.

Use frozenset for immutable lookup tables

When a valid-value set is defined at module level and will never change, use frozenset instead of set. It signals intent clearly — this collection is a constant — and is hashable, so it can itself be stored in sets or used as a dictionary key. Membership testing performance is identical to a regular set.

python
# frozenset: immutable, hashable, O(1) membership — best for module-level constants
VALID_METHODS = frozenset({"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"})

def dispatch(method: str) -> None:
    if method not in VALID_METHODS:
        raise ValueError(f"Unsupported HTTP method: {method!r}")
    print(f"Dispatching {method}")

dispatch("GET")     # Dispatching GET
dispatch("FETCH")   # raises ValueError: Unsupported HTTP method: 'FETCH'

Test integer ranges without building a list

A range object supports in with O(1) time complexity — it never materialises the full sequence in memory. This is far more efficient than writing 0 <= n <= 255 for large spans, and it is substantially better than n in list(range(256)), which converts the range to a list first and runs in O(n).

python
BYTE_RANGE = range(256)  # O(1) membership — no list allocated

def is_valid_byte(value: int) -> bool:
    return value in BYTE_RANGE

print(is_valid_byte(200))   # True
print(is_valid_byte(300))   # False
print(is_valid_byte(-1))    # False

# Also works with steps:
EVEN_BELOW_100 = range(0, 100, 2)
print(42 in EVEN_BELOW_100)   # True
print(43 in EVEN_BELOW_100)   # False

Multi-value membership with any() and a generator

When you need to know whether any of several values is present in a container, chaining or expressions gets unwieldy fast. Using any() with a generator expression is more readable and short-circuits on the first match, so no extra work is done once a hit is found.

python
log_line = "2025-04-05 ERROR disk quota exceeded on /var/log"
critical_keywords = {"ERROR", "CRITICAL", "FATAL", "PANIC"}

# Readable and short-circuits on first match
if any(kw in log_line for kw in critical_keywords):
    print("Alert: critical log entry detected")

# Equivalent but verbose and does not short-circuit cleanly:
# if "ERROR" in log_line or "CRITICAL" in log_line or ...:

# all() works the same way for "every value must be present":
required_fields = {"name", "email", "role"}
submitted = {"name", "email", "role", "department"}
if all(field in submitted for field in required_fields):
    print("All required fields present")

The __contains__ protocol: how in works under the hood

When Python evaluates x in container, it calls container.__contains__(x). If the container does not define __contains__, Python falls back to iterating with __iter__, and if that is also absent, it uses indexed access via __getitem__. Knowing this lets you make your own classes support in naturally.

python
class IPAllowlist:
    """Wraps a set of allowed IP addresses with O(1) membership testing."""

    def __init__(self, *addresses: str) -> None:
        self._allowed: frozenset[str] = frozenset(addresses)

    def __contains__(self, address: str) -> bool:
        return address in self._allowed

    def __repr__(self) -> str:
        return f"IPAllowlist({', '.join(sorted(self._allowed))})"


allowlist = IPAllowlist("192.168.1.1", "10.0.0.1", "172.16.0.5")

print("10.0.0.1" in allowlist)    # True
print("8.8.8.8" in allowlist)     # False
print("8.8.8.8" not in allowlist) # True
Pro Tip

Implementing __contains__ in your own class costs almost nothing and makes the class feel like a first-class Python citizen. Users of your class will write if item in my_obj: rather than calling a custom method like my_obj.has(item), which is more idiomatic and consistent with how every built-in container behaves.

Filtering with not in — and when to use a set intersection instead

A list comprehension with not in against a set is the right choice for filtering a sequence. If you instead need the count of overlapping items, or want to build the filtered result as a set itself, a set difference operation is cleaner and faster than any comprehension.

python
blocked = {"spam_user", "banned_user"}
all_users = ["alice", "bob", "spam_user", "carol", "banned_user"]

# Pattern 1: preserve order, use list comprehension with not in (blocked is already a set)
active_users = [u for u in all_users if u not in blocked]
print(active_users)           # ['alice', 'bob', 'carol']

# Pattern 2: order does not matter — set difference is faster for large inputs
all_set = set(all_users)
active_set = all_set - blocked
print(active_set)             # {'alice', 'bob', 'carol'} (order not guaranteed)

# Pattern 3: how many users are blocked? — set intersection
overlap = all_set & blocked
print(f"{len(overlap)} blocked user(s) found")  # 2 blocked user(s) found
Common Mistake

When testing membership in a dictionary, beginners sometimes expect in to check values. It only checks keys. Use value in my_dict.values() for value lookups, or restructure the dictionary so the values you query become keys — which also restores O(1) performance.

spot the bug click the line that contains the bug

The function below is supposed to check whether the value "active" is stored in the dictionary status. One line contains a bug. Click it, then hit check.

1 def check_status(status):
2 target = "active"
3 # Check if "active" appears as a value
4 if target in status:
5 return True
6 return False
The fix: Change line 4 to if target in status.values():. Using in status on a dictionary tests keys only. Since the intent is to check whether "active" appears as a value, .values() must be called explicitly.

How to Use Membership Operators in Python

Follow these five steps whenever you need to test whether a value belongs to a collection in Python, from choosing the right data structure through to supporting in in your own classes.

  1. Choose the right container

    Select the container type that fits your use case. Use a frozenset for immutable module-level lookup constants. Use range for integer interval checks with O(1) performance and no memory allocation. For dictionaries, in tests keys only by default — use .values() or restructure if you need value lookups.

  2. Write the membership expression

    Place the value to test on the left side of in and the container on the right: value in container. The expression evaluates to True or False and can be used directly in any Boolean context without wrapping in a comparison.

  3. Use the result in a conditional or expression

    Use the membership expression directly in if or while conditions. To test multiple values at once, use any(kw in target for kw in keywords) rather than chained or expressions — it short-circuits on the first match and scales to any number of terms.

  4. Optimise for large or frequent lookups

    For large collections with repeated membership tests, prefer frozenset over list — lookups average O(1). For integer ranges, use range() directly: Python evaluates range membership in O(1) without building the sequence in memory, making it far more efficient than n in list(range(...)).

  5. Implement __contains__ in custom classes

    To make a custom class support the in operator, define a __contains__ method. Python calls container.__contains__(x) when evaluating x in container. If that method is absent, Python falls back to __iter__, then to indexed access via __getitem__. Implementing it explicitly gives you full control over the lookup logic and its performance characteristics.

Figure 1 — How Python evaluates a membership expression: the value is searched in the container and the Boolean result is returned. not in simply inverts the output.
The Python Language Reference specifies that membership test operations evaluate whether a given object belongs to a sequence or collection, always resolving to a Boolean. — Python Language Reference, Expressions, §6.10.2

Python Learning Summary Points

  1. Python provides two membership operators, in and not in, both of which return a Boolean and work across strings, lists, tuples, sets, dictionaries, and any object that implements __contains__.
  2. Dictionary membership with in tests keys by default; to test values use in dict.values(), or restructure so the queried values become keys and restore O(1) lookup.
  3. Prefer frozenset over set for module-level constant lookup tables — it is immutable, hashable, and carries the same O(1) membership performance.
  4. range objects support in with O(1) time complexity and zero memory allocation, making them ideal for integer interval checks over large spans.
  5. Use any(kw in target for kw in keywords) instead of chained or — it short-circuits on the first match and scales cleanly to any number of search terms.
  6. Implement __contains__ in custom classes to make your objects support in natively, matching Python's built-in container behaviour.
  7. When order does not matter and both sides can be sets, set difference (a - b) and intersection (a & b) outperform list comprehensions with not in for large inputs.

With in and not in in your toolkit you can write condition checks, input validators, filters, and range tests in a single readable line. The deeper patterns here — frozenset constants, O(1) range testing, any() short-circuiting, and custom __contains__ — give you a performance-conscious, idiomatic vocabulary for membership logic that scales from simple scripts to production systems.

check your understanding question 1 of 7

Frequently Asked Questions

Python membership operators are in and not in. They test whether a value exists within a sequence or collection such as a string, list, tuple, set, or dictionary, and return a Boolean result of True or False.

The in operator evaluates to True if the left operand is found in the right operand. For example, 3 in [1, 2, 3] returns True because 3 is a member of that list.

When used with a dictionary, in checks against the dictionary's keys, not its values. To check values, use in dict.values(), and to check key-value pairs use in dict.items().

Sets use a hash table internally, so membership testing runs in O(1) average time regardless of size. Lists require a linear scan of every element, which is O(n) time. For large collections tested frequently, sets offer a significant performance advantage.

Yes. When testing membership in strings, in and not in are case-sensitive. "hello" in "Hello World" returns False because the lowercase h does not match the uppercase H.

Yes. Because in and not in return Boolean values, they are commonly used directly in if and while conditions. For example: if user in allowed_users: grant_access().

The not in operator returns True when the left operand is not found in the right operand. It is the logical inverse of in. For example, 5 not in [1, 2, 3] returns True.

When used with strings, in performs a substring search. It returns True if the left string appears anywhere inside the right string. The check is case-sensitive by default, and any non-empty string is a valid substring to search for.

Python does not support chaining membership operators the way comparison operators can be chained. To test multiple values you combine in expressions with and or or: if x in group and y in group. For a cleaner approach when checking several values, use any(v in container for v in values).

in is a membership operator that tests whether a value exists inside a container. is is an identity operator that tests whether two variable names refer to the exact same object in memory. They serve fundamentally different purposes and should not be used interchangeably.

A frozenset is an immutable, hashable version of a set. It provides the same O(1) average membership testing as a regular set and is the preferred choice for module-level constant lookup tables because it cannot be modified accidentally and can itself be stored in sets or used as a dictionary key. Declare it once at the top of your module and every in test against it will run in constant time.

When Python evaluates x in container, it calls container.__contains__(x). If the class does not define __contains__, Python falls back to iterating with __iter__, then to indexed access via __getitem__. Defining __contains__ explicitly gives you full control over the membership logic and its performance characteristics, and makes your class feel like a native Python container to anyone using it.

Certificate of Completion
Final Exam
Pass mark: 80% · Score 80% or higher to receive your certificate

Enter your name as you want it to appear on your certificate, then start the exam. Your name is used only to generate your certificate and is never transmitted or stored anywhere.

Question 1 of 10 Score: 0