Why does "a == x or y or z" always evaluate to True?
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.
-
Use multiple
==
operators to explicitly check against each value:if name == "Kevin" or name == "Jon" or name == "Inbar":
-
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
.
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.")
Simple engineering problem, let's simply it a bit further.
In [1]: a,b,c,d=1,2,3,4
In [2]: a==b
Out[2]: False
But, inherited from the language C, Python evaluates the logical value of a non zero integer as True.
In [11]: if 3:
...: print ("yey")
...:
yey
Now, Python builds on that logic and let you use logic literals such as or on integers, and so
In [9]: False or 3
Out[9]: 3
Finally
In [4]: a==b or c or d
Out[4]: 3
The proper way to write it would be:
In [13]: if a in (b,c,d):
...: print('Access granted')
For safety I'd also suggest you don't hard code passwords.
Not-empty lists, sets, strings, etc. are evaluable and, therefore, return True.
Therefore, when you say:
a = "Raul"
if a == "Kevin" or "John" or "Inbar":
pass
You are actually saying:
if "Raul" == "Kevin" or "John" != "" or "Inbar" != "":
pass
Since at least one of "John" and "Inbar" is not an empty string, the whole expression always returns True!
The solution:
a = "Raul"
if a == "Kevin" or a == "John" or a == "Inbar":
pass
or:
a = "Raul"
if a in {"Kevin", "John", "Inbar"}:
pass