Logical, Membership and Identity Operators
and, or and not return one of their operands rather than a boolean. in tests membership. is tests identity. Each has a use where the others are wrong.
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
Logical operators
| Operator | Reads as | True when |
|---|---|---|
and | both | Both sides are truthy |
or | either | At least one side is truthy |
not | the opposite of | The operand is falsy |
age = 25
has_id = True
print(age >= 18 and has_id) # True
print(age < 18 or has_id) # True
print(not has_id) # FalsePython spells these as words rather than as &&, || and !. That is a readability decision, and the symbols &, | and ~ mean something different, which the bitwise note covers.
They do not return booleans
This is the part that surprises people, and it is genuinely useful once understood.
print(0 or "default") # default
print("value" or "default") # value
print("a" and "b") # b
print(0 and "b") # 0
print("" or [] or "last") # lastThe rules are exact:
a and breturnsaifais falsy, otherwiseb.a or breturnsaifais truthy, otherwiseb.not ais the only one of the three that always returns a realTrueorFalse.
The default value idiom
def greet(name):
name = name or "guest"
return "Hello, " + name
print(greet("Meera")) # Hello, Meera
print(greet("")) # Hello, guestThis idiom has a sharp edge. It replaces every falsy value, so0,0.0,Falseand[]are all overwritten by the default. When zero is a legitimate value, test explicitly withif name is None:instead.
Short circuit evaluation
Python stops evaluating as soon as the answer is settled. The right hand side may never run at all.
def expensive():
print("expensive() ran")
return True
print(False and expensive()) # prints False. expensive() never ran.
print(True or expensive()) # prints True. expensive() never ran.Using short circuiting as a guard
values = []
if len(values) > 0 and values[0] > 10: # safe
print("first value is large")
# if values[0] > 10 and len(values) > 0: # IndexError - order mattersconfig = {"retries": 3}
# Safe lookup: the second part only runs if the key exists.
if "timeout" in config and config["timeout"] > 0:
print("timeout configured")Put the cheap test, or the test that makes the next one safe, on the left. That ordering is not style; it is what prevents the error.
Membership operators
names = ["Meera", "Arun", "Sara"]
print("Arun" in names) # True
print("Ravi" not in names) # True
print("th" in "Python") # True - substring test
print(3 in (1, 2, 3)) # True
print("a" in {"a": 1}) # True - dictionaries test the KEYS
print(1 in {"a": 1}) # False - not the valuesMembership cost differs by type
| Container | How in works | Cost |
|---|---|---|
list, tuple | Compares each element in turn | Proportional to length |
str | Substring search | Proportional to length |
set, frozenset | Hash lookup | Roughly constant |
dict | Hash lookup on the keys | Roughly constant |
blocked_list = ["a", "b", "c"] # membership scans
blocked_set = {"a", "b", "c"} # membership is a hash lookup
# For a handful of items either is fine.
# For tens of thousands of items, checked in a loop, the set is the difference
# between a fast program and an unusable one.Identity operators
a = [1, 2]
b = a
c = [1, 2]
print(a is b) # True - one object, two names
print(a is c) # False - two objects
print(a == c) # True - equal contents
print(a is not c) # TrueWhen is is correct
value = None
print(value is None) # the standard test
flag = True
print(flag is True) # valid, though `if flag:` is preferred
def find(items, target):
for item in items:
if item is target: # deliberate: the same object, not merely equal
return True
return FalseUse is for None, for the two boolean singletons, and where you genuinely mean "the same object". Everywhere else use ==. Comparing strings or numbers with is appears to work because of interpreter caching, and then fails on values that are not cached.
Combining them
user = {"name": "Meera", "role": "admin", "active": True}
if user is not None and user.get("active") and user.get("role") in ("admin", "owner"):
print("Access granted")Read that left to right: exists, is active, has an acceptable role. Each test protects the one after it.
Common mistakes
- Using
&&,||or!. Python uses words, and the symbols mean bitwise operations. - Writing
if x == 1 or 2:. That is always true, because2alone is truthy. Writeif x in (1, 2):. - Using
orfor defaults where0or""is a valid value. - Using
isto compare values. - Testing membership against a large list inside a loop instead of converting it to a set once.
- Assuming
inon a dictionary looks at the values.
Best practices
- Order the parts of an
andso the cheap or protective test comes first. - Use
inwith a tuple instead of a chain oforcomparisons. - Convert a repeatedly searched list into a set once, before the loop.
- Reserve
isforNoneand true identity checks. - Use
notrather than comparing toFalse.
Practice
- Predict and explain:
print([] or 0 or "x" or None). - Explain why
if name == "a" or "b":matches everything, and rewrite it correctly. - Write a safe condition that checks the first element of a possibly empty list.
- Explain the difference in cost between
x in big_listandx in big_set, and when it stops mattering. - Give one situation where
value or defaultgives the wrong answer, and rewrite it properly.
Conclusion
and and or return an operand, not a boolean, and they stop early. in asks about contents and its cost depends entirely on the container. is asks about identity and is almost always the wrong tool unless the answer is None.