Categories
boolean boolean-expression python

Why does “a == x or y or z” always evaluate to True?

147

I am writing a security system that denies access to unauthorized users.

name = input("Hello. Please enter your name: ")
if name == "Kevin" or "Jon" or "Inbar":
    print("Access granted.")
else:
    print("Access denied.")

It grants access to authorized users as expected, but it also lets in unauthorized users!

Hello. Please enter your name: Bob
Access granted.

Why does this occur? I’ve plainly stated to only grant access when name equals Kevin, Jon, or Inbar. I have also tried the opposite logic, if "Kevin" or "Jon" or "Inbar" == name, but the result is the same.


Note: this question is intended as the canonical duplicate target of this very common problem. There is another popular question How to test multiple variables for equality against a single value? that has the same fundamental problem, but the comparison targets are reversed. This question should not be closed as a duplicate of that one as this problem is encountered by newcomers to Python who might have difficulties applying the knowledge from the reversed question to their problem.

2

  • 11

    Variations of this problem include x or y in z, x and y in z, x != y and z and a few others. While not exactly identical to this question, the root cause is the same for all of them. Just wanted to point that out in case anyone got their question closed as duplicate of this and wasn’t sure how it’s relevant to them.

    – Aran-Fey

    Apr 11, 2019 at 8:55

  • See also e.g. stackoverflow.com/questions/17902492/… .

    Feb 18 at 6:27

196

In many cases, Python looks and behaves like natural English, but this is one case where that abstraction fails. People can use context clues to determine that “Jon” and “Inbar” are objects joined to the verb “equals”, but the Python interpreter is more literal minded.

if name == "Kevin" or "Jon" or "Inbar":

is logically equivalent to:

if (name == "Kevin") or ("Jon") or ("Inbar"):

Which, for user Bob, is equivalent to:

if (False) or ("Jon") or ("Inbar"):

The or operator chooses the first argument with a positive truth value:

if "Jon":

And since “Jon” has a positive truth value, the if block executes. That is what causes “Access granted” to be printed regardless of the name given.

All of this reasoning also applies to the expression if "Kevin" or "Jon" or "Inbar" == name. the first value, "Kevin", is true, so the if block executes.


There are two common ways to properly construct this conditional.

  1. Use multiple == operators to explicitly check against each value:

    if name == "Kevin" or name == "Jon" or name == "Inbar":
    
  2. Compose a collection of valid values (a set, a list or a tuple for example), and use the in operator to test for membership:

    if name in {"Kevin", "Jon", "Inbar"}:
    

In general of the two the second should be preferred as it’s easier to read and also faster:

>>> import timeit
>>> timeit.timeit('name == "Kevin" or name == "Jon" or name == "Inbar"',
    setup="name="Inbar"")
0.4247764749999945
>>> timeit.timeit('name in {"Kevin", "Jon", "Inbar"}', setup="name="Inbar"")
0.18493307199999265

For those who may want proof that if a == b or c or d or e: ... is indeed parsed like this. The built-in ast module provides an answer:

>>> import ast
>>> ast.parse("a == b or c or d or e", "<string>", "eval")
<ast.Expression object at 0x7f929c898220>
>>> print(ast.dump(_, indent=4))
Expression(
    body=BoolOp(
        op=Or(),
        values=[
            Compare(
                left=Name(id='a', ctx=Load()),
                ops=[
                    Eq()],
                comparators=[
                    Name(id='b', ctx=Load())]),
            Name(id='c', ctx=Load()),
            Name(id='d', ctx=Load()),
            Name(id='e', ctx=Load())]))

As one can see, it’s the boolean operator or applied to four sub-expressions: comparison a == b; and simple expressions c, d, and e.

4

  • 1

    Is there a specific reason to choose a tuple ("Kevin", "Jon", "Inbar") instead of a set {"Kevin", "Jon", "Inbar"} ?

    – Human

    Jun 14, 2019 at 12:04

  • 3

    Not really, since both work if the values are all hashable. Set membership testing has better big-O complexity than tuple membership testing, but constructing a set is a little more expensive than constructing a tuple. I think it’s largely a wash for small collections like these. Playing around with timeit, a in {b, c, d} is about twice as fast as a in (b, c, d) on my machine. Something to think about if this is a performance-critical piece of code.

    – Kevin

    Jun 14, 2019 at 12:13

  • 3

    Tuple or list when using ‘in’ in an ‘if’ clause? recommends set literals for membership testing. I’ll update my post.

    – Kevin

    Jun 14, 2019 at 12:20

  • 3

    In modern Python, it recognizes that the set is a constant and makes it a frozenset instead, so the constructing set overhead is not there. dis.dis(compile("1 in {1, 2, 3}", '<stdin>', 'eval'))

    – endolith

    May 16, 2020 at 17:48


4

There are 3 condition checks in if name == "Kevin" or "Jon" or "Inbar":

  • name == “Kevin”
  • “Jon”
  • “Inbar”

and this if statement is equivalent to

if name == "Kevin":
    print("Access granted.")
elif "Jon":
    print("Access granted.")
elif "Inbar":
    print("Access granted.")
else:
    print("Access denied.")

Since elif "Jon" will always be true so access to any user is granted

Solution


You can use any one method below

Fast

if name in ["Kevin", "Jon", "Inbar"]:
    print("Access granted.")
else:
    print("Access denied.")

Slow

if name == "Kevin" or name == "Jon" or name == "Inbar":
    print("Access granted.")
else:
    print("Access denied.")

Slow + Unnecessary code

if name == "Kevin":
    print("Access granted.")
elif name == "Jon":
    print("Access granted.")
elif name == "Inbar":
    print("Access granted.")
else:
    print("Access denied.")

0

    4

    Summarising all existing answers

    (And adding a few of my points)

    Explanation :

    if name == "Kevin" or "Jon" or "Inbar":
    

    is logically equivalent to:

    if (name == "Kevin") or ("Jon") or ("Inbar"):
    

    Which, for user Bob, is equivalent to:

    if (False) or ("Jon") or ("Inbar"):
    

    NOTE : Python evaluates the logical value of any non-zero integer as True. Therefore, all Non-empty lists, sets, strings, etc. are evaluable and return True

    The or operator chooses the first argument with a positive truth value.

    Therefore, “Jon” has a positive truth value and the if block executes, since it is now equivalent to

    if (False) or (True) or (True):
    

    That is what causes “Access granted” to be printed regardless of the name input.

    Solutions :

    Solution 1 : Use multiple == operators to explicitly check against each value

    if name == "Kevin" or name == "Jon" or name == "Inbar":
        print("Access granted.")
    else:
        print("Access denied.")
    

    Solution 2 : Compose a collection of valid values (a set, a list or a tuple for example), and use the in operator to test for membership (faster, preferred method)

    if name in {"Kevin", "Jon", "Inbar"}:
        print("Access granted.")
    else:
        print("Access denied.")
    

    OR

    if name in ["Kevin", "Jon", "Inbar"]:
        print("Access granted.")
    else:
        print("Access denied.")
    

    Solution 3 : Use the basic (and not very efficient) if-elif-else structure

    if name == "Kevin":
        print("Access granted.")
    elif name == "Jon":
        print("Access granted.")
    elif name == "Inbar":
        print("Access granted.")
    else:
        print("Access denied.")