🚀 Supercharge your YouTube channel's growth with AI.
Try YTGrowAI FreePython in Operator and not in for Membership Tests

Define a custom contains method on your own class and the in operator will call it instead of scanning anything. Print one line inside that method and you can watch Python delegate every membership check, which is the whole mechanism behind in and not in. The same handoff is why a list check walks every element while a set check is nearly instant.
How the Python in operator finds a match
Python asks the container how membership works, so a class can answer without storing or scanning a collection. Write the item on the left and the container on the right.
With your Python environment in venv, save this as contains.py and run ./venv/bin/python contains.py to watch the delegation happen.
class Allowed:
def __contains__(self, item):
print("__contains__ called for", repr(item))
return item == "timeout"
allowed = Allowed()
print("timeout" in allowed)
print("colour" in allowed)
The method receives the candidate and Python converts its answer to a Boolean, printing the trace line before each result. Change the equality check and membership changes.
Remove __contains__ and Python falls back to __iter__, so this class hands out a fresh iterator on every search.
class IterableOptions:
def __iter__(self):
print("__iter__ called")
return iter(["timeout", "retries"])
options = IterableOptions()
print("colour" in options)
print("retries" in options)
A failed search prints the call and returns False, while the second search stops at the equal item and returns True. Never return one shared iterator here.
With neither method defined, integer indexing through __getitem__ is the last fallback, starting at zero.
class IndexedOptions:
def __getitem__(self, index):
print("index", index)
return ["timeout", "retries"][index]
print("colour" in IndexedOptions())
print("__contains__" in list.__dict__)
The missing name walks indexes 0, 1, and 2 before IndexError ends the search with False. Lists skip this walk because they define __contains__ directly.

Use the Python in operator with built-in containers
A membership test answers whether a match exists, never where it sits. Keep a list or tuple when order matters, a set for unique allowed entries, and a dictionary when each key owns a value.
Run these whole-name checks against a list with a duplicate entry, then against the same names as a tuple.
options = ["timeout", "retries", "timeout"]
print("timeout" in options, "colour" in options)
print("timeout" in tuple(options), "time" in options)
Both containers answer True then False, so neither the duplicate nor the list-to-tuple switch changes anything. The partial name time fails because tuple membership compares whole elements, never substrings inside them.
Test one key against a set of names and one value against a dictionary to separate membership from retrieval.
allowed = {"timeout", "retries", "timeout"}
config = {"timeout": 30, "retries": 3}
print("timeout" in allowed, "colour" in allowed)
print("timeout" in config, 30 in config)
The set keeps a single copy of timeout and the dictionary holds its keys apart from their values. Since the number 30 lives only as a value, dictionary membership rejects it, and you look the value up with square brackets after the key test succeeds.
Search a comma-joined option string for a partial name, an uppercase name, and an empty candidate.
text = "timeout,retries"
print("time" in text, "TIME" in text, "" in text)
print("TIME".casefold() in text.casefold())
print("time" in text.split(","))
Substring matching makes time and the empty string match while case-sensitive TIME fails. Split the text into complete names when partial matches must not count, and reach for casefold only when your policy says letter case is irrelevant.
Test dictionary keys, values, or pairs deliberately
The expression key in config searches the dictionary itself, never its values. Calling values() or items() swaps in a different object to search, which lets you choose values or complete key-value pairs without copying anything.
Ask one dictionary for a key, then for its value, then ask its values view for that value.
config = {"timeout": 30, "retries": 3}
print("timeout" in config)
print(30 in config)
print(30 in config.values())
The key test passes, the bare value test fails, and the values-view test passes. Read the middle False as an answer to the wrong question, since values can repeat under keys you cannot name from the result alone.
Loop over items with a condition when you need the key attached to a matching value.
config = {"timeout": 30, "retries": 3}
for key, value in config.items():
if value == 30:
print("matched", key)
break
else:
print("no matching value")
Break stops at the first match and the loop else covers the no-match path. Drop the break and collect keys instead when the task needs every option holding the requested value.
Check an item tuple when the key and its value must agree together, in key-then-value order.
config = {"timeout": 30, "retries": 3}
print(("timeout", 30) in config.items())
print(("timeout", 3) in config.items())
print((30, "timeout") in config.items())
Only the exact pair passes, and reversing the tuple fails despite showing the same two objects. The Python dictionary tutorial explains these views and how updates change what they contain.
Use not in to reject an unsupported option
Absence should trigger the branch when you guard input, so not in keeps the item and its container together. The spelling not x in y runs but splits the negation away from the operator and reads worse.
Negate one membership result three ways against an unchanged set to compare the spellings.
allowed = {"timeout", "retries"}
key = "colour"
print(key not in allowed)
print(not key in allowed)
print(not (key in allowed))
All three print True here. Prefer key not in allowed in application code, and never insert is between the item and not in.
Return early for an unsupported key so the successful path stays at the top level of the function.
def check_key(key):
allowed = {"timeout", "retries"}
if key not in allowed:
return "unsupported"
return "accepted"
print(check_key("colour"))
print(check_key("timeout"))
Colour exits through the guard while timeout reaches the accepted path. Returning stops only this call, so raise an exception at the caller when configuration loading itself must halt.
Combine two complete membership tests when both options are required, and watch what a bare string does.
config = {"timeout": 30}
print("timeout" in config and "retries" in config)
print("timeout" in config or "retries" in config)
print(bool("retries" or "timeout" in config))
The and form demands both keys and the or form accepts either, giving False then True. A bare nonempty string is truthy on its own, which is why the last line stays True without testing the missing key.
Validate a configuration against an allowlist
Start from timeout, retries, and color as the allowed names. An input dictionary arrives carrying the unsupported spelling colour, and no setting gets applied before that name is resolved.
Split an input dictionary into accepted settings and unknown names without mutating the input.
config = {"timeout": 30, "retries": 3, "colour": "blue"}
allowed = {"timeout", "retries", "color"}
accepted = {key: value for key, value in config.items() if key in allowed}
rejected = sorted(key for key in config if key not in allowed)
print("accepted", accepted)
print("rejected", rejected)
Accepted keeps timeout and retries while rejected names colour in sorted order. Never apply the accepted part silently, since that would discard the requested color setting.
Fix the spelling on a copy while the original input stays intact for diagnostics.
config = {"timeout": 30, "retries": 3, "colour": "blue"}
allowed = {"timeout", "retries", "color"}
corrected = dict(config)
corrected["color"] = corrected.pop("colour")
print("unknown", sorted(set(corrected) - allowed))
print("original unchanged", "colour" in config)
Set difference reports the supplied names missing from the allowlist, and here it comes back empty. That empty result blesses the names only, never the value types.
Save the complete validator as validate_config.py and run ./venv/bin/python validate_config.py to see rejection precede acceptance.
ALLOWED = {"timeout", "retries", "color"}
def validate_config(config):
rejected = sorted(key for key in config if key not in ALLOWED)
if rejected:
raise ValueError("Unknown keys: " + ", ".join(rejected))
return dict(config)
config = {"timeout": 30, "retries": 3, "colour": "blue"}
try:
validate_config(config)
except ValueError as error:
print("Rejected:", error)
corrected = dict(config)
corrected["color"] = corrected.pop("colour")
print("Accepted:", validate_config(corrected))
The first call raises on colour and the corrected dictionary comes back accepted with all three settings. Callers receive a copy only after validation succeeds.

Measure list, set, and dictionary membership
A list compares the candidate against every element, which slows as the container grows. Sets and dictionary keys hash the candidate first, so average lookup stays roughly flat.
Save this as benchmark.py and run ./venv/bin/python benchmark.py to time a missing option across prebuilt containers.
import platform
from timeit import repeat
names = [f"option_{i}" for i in range(10000)]
containers = {"list": names, "set": set(names), "dict": dict.fromkeys(names)}
print("Python", platform.python_version())
for label, container in containers.items():
seconds = min(repeat("'missing' in container", globals={"container": container}, number=10000, repeat=5))
print(f"{label}: {seconds / 10000 * 1e6:.3f} us/lookup")
This run put the list near one hundred microseconds per lookup against small fractions of a microsecond for the set and dictionary. Exact timings move between machines and runs, while the difference between a full scan and a hash lookup persists.
For the small configuration allowlist, build the set once and reuse it across checks.
allowed = {"timeout", "retries", "color"}
config = {"timeout": 30, "retries": 3, "colour": "blue"}
print([key for key in config if key not in allowed])
print("timeout" in list(allowed), "timeout" in allowed)
Both representations agree on timeout and the comprehension still catches colour. Rebuilding the set inside a validation loop would tax every check.
Search a fresh generator to see what membership consumes, then inspect what remains.
keys = (key for key in ["timeout", "retries", "color"])
print("retries" in keys)
print(list(keys))
print("timeout" in keys)
The first search consumes through retries and returns True, leaving only color behind. Prefer a bounded reusable container for an allowlist, because a failed search on an unbounded iterator might never terminate.

Separate membership, equality, and identity
Membership asks whether a container accepts a candidate, equality compares two values, and is tests object identity. Identity means two references point at one object, not merely at equal contents.
Build two separate lists with equal contents, then search for one inside a container holding the other.
first = ["timeout"]
second = ["timeout"]
print(first == second)
print(first is second)
print(second in [first])
Equality passes, identity fails, and membership succeeds through equality. Reserve is for the case where your condition needs that particular object, such as a sentinel, rather than another equal one.
Test how 1, True, and a set of numbers interact before assuming a hit proves the candidate type.
print(1 == True)
print(1 in [True])
print(True in {1})
All three print True because bool subclasses int and equal numbers hash alike. State a separate type requirement when the setting demands an integer and not a Boolean.
Require the exact type alongside the value when a setting must hold an integer rather than a Boolean.
values = [True, 2]
print(any(type(value) is int and value == 1 for value in values))
print(any(type(value) is int and value == 2 for value in values))
sentinel = object()
print(any(value is sentinel for value in [sentinel]))
The first check rejects the Boolean 1 while the second accepts the integer 2, and any stops at the first True it meets. Choose isinstance instead when subclasses should pass, adding an explicit bool exclusion when necessary.
Define custom membership and handle unhashable inputs
A custom container can define membership around the field your application uses, such as an option name. Document whether searching consumes state or needs a hashable candidate, since callers cannot tell either fact from the expression alone.
Store option records but search by name, delegating repeated lookups to a set built once.
class OptionAllowlist:
def __init__(self, records):
self.names = {record["name"] for record in records}
def __contains__(self, name):
return name in self.names
allowed = OptionAllowlist([{"name": "timeout"}, {"name": "retries"}, {"name": "color"}])
config = {"timeout": 30, "retries": 3, "colour": "blue"}
print("timeout" in allowed)
print(sorted(key for key in config if key not in allowed))
Python locates __contains__ on the class and the name lookup runs against the internal set. The data model reference documents this hook and the iteration fallbacks that apply when it is absent.
Save this as unhashable.py and run ./venv/bin/python unhashable.py to contrast a list with an immutable tuple.
for operation in (lambda: {["timeout", "retries"]}, lambda: ["timeout", "retries"] in {("timeout", "retries")}):
try:
print(operation())
except TypeError as error:
print(type(error).__name__ + ":", error)
print(("timeout", "retries") in {("timeout", "retries")})
Both list operations raise TypeError while the tuple lookup returns True. Conversion rescues the code only when every nested element is hashable too, since a tuple holding a list stays unhashable.
Keep a plain list container when membership must compare mutable option groups directly.
allowed_groups = [["timeout", "retries"], ["color"]]
print(["timeout", "retries"] in allowed_groups)
print(["retries", "timeout"] in allowed_groups)
The first group matches and the reordered group fails, because sequence equality respects element order. Favor a frozenset for an unordered group only when all of its members are hashable.

How do I check whether a value exists in a dictionary?
Use value in config.values() to search values, or key in config to search keys. If you need the matching key as well, iterate over config.items() and compare each value, remembering that several keys can hold equal values, so collect every match before choosing.
What is the difference between in and is?
The in operator performs a membership test under the container rules, usually matching identical or equal elements. The is operator compares object identity instead, so two separately created lists can be equal and match through membership while remaining different objects.
Does in work on a Python string?
Yes, it checks for a contiguous substring with case-sensitive matching, and an empty string matches every string. For complete option names, split the text into separate complete names before searching, rather than always letting partial substrings count as accepted options.
Why is set membership usually faster than list membership?
A set uses a hash table to locate possible matches instead of scanning the whole sequence, giving average constant-time lookup for inexpensive hashes and equality checks. Building the set has a cost, collisions can degrade lookup, and short lists may be fast enough without conversion.
Can I use in on my own class?
Define __contains__(self, item) to choose what membership means for your class, and return a Boolean answer. Without that method Python tries iteration, then integer-indexed access, so always document clearly whether searching your object consumes state or requires a hashable candidate.


