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.

Logical operators

OperatorReads asTrue when
andbothBoth sides are truthy
oreitherAt least one side is truthy
notthe opposite ofThe 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)               # False

Python 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")    # last

The rules are exact:

  • a and b returns a if a is falsy, otherwise b.
  • a or b returns a if a is truthy, otherwise b.
  • not a is the only one of the three that always returns a real True or False.

The default value idiom

def greet(name):
    name = name or "guest"
    return "Hello, " + name


print(greet("Meera"))    # Hello, Meera
print(greet(""))         # Hello, guest
This idiom has a sharp edge. It replaces every falsy value, so 0, 0.0, False and [] are all overwritten by the default. When zero is a legitimate value, test explicitly with if 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 matters
config = {"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 values

Membership cost differs by type

ContainerHow in worksCost
list, tupleCompares each element in turnProportional to length
strSubstring searchProportional to length
set, frozensetHash lookupRoughly constant
dictHash lookup on the keysRoughly 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)      # True

When 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 False

Use 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, because 2 alone is truthy. Write if x in (1, 2):.
  • Using or for defaults where 0 or "" is a valid value.
  • Using is to compare values.
  • Testing membership against a large list inside a loop instead of converting it to a set once.
  • Assuming in on a dictionary looks at the values.

Best practices

  • Order the parts of an and so the cheap or protective test comes first.
  • Use in with a tuple instead of a chain of or comparisons.
  • Convert a repeatedly searched list into a set once, before the loop.
  • Reserve is for None and true identity checks.
  • Use not rather than comparing to False.

Practice

  1. Predict and explain: print([] or 0 or "x" or None).
  2. Explain why if name == "a" or "b": matches everything, and rewrite it correctly.
  3. Write a safe condition that checks the first element of a possibly empty list.
  4. Explain the difference in cost between x in big_list and x in big_set, and when it stops mattering.
  5. Give one situation where value or default gives 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.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.