4
print 'a' in 'ab'

prints True, while

print 'a' in 'ab' == True

prints False.

Any guess why?

arshajii
  • 127,459
  • 24
  • 238
  • 287
Lokesh
  • 2,842
  • 7
  • 32
  • 47

3 Answers3

5
>>> 'a' in 'ab' == True
False
>>> ('a' in 'ab') == True
True

Let's take a look at what the first variant actually means:

>>> import dis
>>> def f():
...     'a' in 'ab' == True
... 
>>> dis.dis(f)
  2           0 LOAD_CONST               1 ('a')
              3 LOAD_CONST               2 ('ab')
              6 DUP_TOP             
              7 ROT_THREE           
              8 COMPARE_OP               6 (in)
             11 JUMP_IF_FALSE_OR_POP    23
             14 LOAD_GLOBAL              0 (True)
             17 COMPARE_OP               2 (==)
             20 JUMP_FORWARD             2 (to 25)
        >>   23 ROT_TWO             
             24 POP_TOP             
        >>   25 POP_TOP             
             26 LOAD_CONST               0 (None)
             29 RETURN_VALUE  

If you follow the bytecode, you will see that this equates to 'a' in 'ab' and 'ab' == True.

  • After the first two LOAD_CONSTs, the stack consists of:
    • 'ab'
    • 'a'
  • After DUP_TOP, the stack consists of:
    • 'ab'
    • 'ab'
    • 'a'
  • After ROT_THREE, the stack consists of:
    • 'ab'
    • 'a'
    • 'ab'
  • At this point, in (COMPARE_OP) operates on the top two elements, as in 'a' in 'ab'. The result of this (True) is stored on the stack, while 'ab' and 'a' are popped off. Now the stack consists of:
    • True
    • 'ab'
  • JUMP_IF_FALSE_OR_POP is the short-circuiting of and: if the value on top of the stack at this point is False we know that the entire expression will be False, so we skip over the second upcoming comparison. In this case, however, the top value is True, so we pop this value and continue on to the next comparison. Now the stack consists of:
    • 'ab'
  • LOAD_GLOBAL loads True, and COMPARE_OP (==) compares this with the remaining stack item, 'ab', as in 'ab' == True. The result of this is stored on the stack, and this is the result of the entire statement.

(you can find a full list of bytecode instructions here)

Putting it all together, we have 'a' in 'ab' and 'ab' == True.

Now, 'ab' == True is False:

>>> 'ab' == True
False

meaning the entire expression will be False.

arshajii
  • 127,459
  • 24
  • 238
  • 287
5

Operator chaining at work.

'a' in 'ab' == True

is equivalent to

'a' in 'ab' and 'ab' == True

Take a look:

>>> 'a' in 'ab' == True
False
>>> ('a' in 'ab') == True
True
>>> 'a' in ('ab' == True)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: argument of type 'bool' is not iterable
>>> 'a' in 'ab' and 'ab' == True
False

From the docs linked above:

Comparisons can be chained arbitrarily, e.g., x < y <= z is equivalent to x < y and y <= z, except that y is evaluated only once (but in both cases z is not evaluated at all when x < y is found to be false).

Formally, if a, b, c, ..., y, z are expressions and op1, op2, ..., opN are comparison operators, then a op1 b op2 c ... y opN z is equivalent to a op1 b and b op2 c and ... y opN z, except that each expression is evaluated at most once.

The real advantage of operator chaining is that each expression is evaluated once, at most. So with a < b < c, b is only evaluated once and then compared first to a and secondly (if necesarry) to c.

As a more concrete example, lets consider the expression 0 < x < 5. Semantically, we mean to say that x is in the closed range [0,5]. Python captures this by evaluating the logically equivalent expression 0 < x and x < 5. Hope that clarifies the purpose of operator chaining somewhat.

Brian
  • 3,091
  • 16
  • 29
2

The reason is that Python treats a <op1> b <op2> c as a <op1> b and b <op2> c. For example, 0 < i < n is true if 0 < i and i < n. So the version without parentheses checks whether 'a' in 'ab' (this part is true) and 'ab' == True (this part is false, so the whole expression is false).

The "simple" fix is a pair of parentheses because that is an escape hatch to the behavior described above, but a better option is just not comparing with booleans explicitly.