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.
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:
# 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.
role = "admin"
allowed_roles = {"admin", "editor", "viewer"} # set: O(1) lookup
if role in allowed_roles:
print("Access granted")
else:
print("Access denied")
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.
Build a membership test that checks whether "python" is in the list called languages:
"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, usein 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).
# 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.
# 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).
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.
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.
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
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.
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
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.
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.
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.
-
Choose the right container
Select the container type that fits your use case. Use a
frozensetfor immutable module-level lookup constants. Userangefor integer interval checks with O(1) performance and no memory allocation. For dictionaries,intests keys only by default — use.values()or restructure if you need value lookups. -
Write the membership expression
Place the value to test on the left side of
inand the container on the right:value in container. The expression evaluates toTrueorFalseand can be used directly in any Boolean context without wrapping in a comparison. -
Use the result in a conditional or expression
Use the membership expression directly in
iforwhileconditions. To test multiple values at once, useany(kw in target for kw in keywords)rather than chainedorexpressions — it short-circuits on the first match and scales to any number of terms. -
Optimise for large or frequent lookups
For large collections with repeated membership tests, prefer
frozensetoverlist— lookups average O(1). For integer ranges, userange()directly: Python evaluates range membership in O(1) without building the sequence in memory, making it far more efficient thann in list(range(...)). -
Implement __contains__ in custom classes
To make a custom class support the
inoperator, define a__contains__method. Python callscontainer.__contains__(x)when evaluatingx 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.
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
- Python provides two membership operators,
inandnot in, both of which return a Boolean and work across strings, lists, tuples, sets, dictionaries, and any object that implements__contains__. - Dictionary membership with
intests keys by default; to test values usein dict.values(), or restructure so the queried values become keys and restore O(1) lookup. - Prefer
frozensetoversetfor module-level constant lookup tables — it is immutable, hashable, and carries the same O(1) membership performance. rangeobjects supportinwith O(1) time complexity and zero memory allocation, making them ideal for integer interval checks over large spans.- Use
any(kw in target for kw in keywords)instead of chainedor— it short-circuits on the first match and scales cleanly to any number of search terms. - Implement
__contains__in custom classes to make your objects supportinnatively, matching Python's built-in container behaviour. - When order does not matter and both sides can be sets, set difference (
a - b) and intersection (a & b) outperform list comprehensions withnot infor 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.
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.