Multiple same-line is operators [duplicate] - python

Looking into Queue.py in Python 2.6, I found this construct that I found a bit strange:
def full(self):
"""Return True if the queue is full, False otherwise
(not reliable!)."""
self.mutex.acquire()
n = 0 < self.maxsize == self._qsize()
self.mutex.release()
return n
If maxsize is 0 the queue is never full.
My question is how does it work for this case? How 0 < 0 == 0 is considered False?
>>> 0 < 0 == 0
False
>>> (0) < (0 == 0)
True
>>> (0 < 0) == 0
True
>>> 0 < (0 == 0)
True

I believe Python has special case handling for sequences of relational operators to make range comparisons easy to express. It's much nicer to be able to say 0 < x <= 5 than to say (0 < x) and (x <= 5).
These are called chained comparisons. And that's a link to the documentation for them.
With the other cases you talk about, the parentheses force one relational operator to be applied before the other, and so they are no longer chained comparisons. And since True and False have values as integers you get the answers you do out of the parenthesized versions.

Because
(0 < 0) and (0 == 0)
is False. You can chain together comparison operators and they are automatically expanded out into the pairwise comparisons.
EDIT -- clarification about True and False in Python
In Python True and False are just instances of bool, which is a subclass of int. In other words, True really is just 1.
The point of this is that you can use the result of a boolean comparison exactly like an integer. This leads to confusing things like
>>> (1==1)+(1==1)
2
>>> (2<1)<1
True
But these will only happen if you parenthesise the comparisons so that they are evaluated first. Otherwise Python will expand out the comparison operators.

The strange behavior your experiencing comes from pythons ability to chain conditions. Since it finds 0 is not less than 0, it decides the entire expression evaluates to false. As soon as you break this apart into seperate conditions, you're changing the functionality. It initially is essentially testing that a < b && b == c for your original statement of a < b == c.
Another example:
>>> 1 < 5 < 3
False
>>> (1 < 5) < 3
True

>>> 0 < 0 == 0
False
This is a chained comparison. It returns true if each pairwise comparison in turn is true. It is the equivalent to (0 < 0) and (0 == 0)
>>> (0) < (0 == 0)
True
This is equivalent to 0 < True which evaluates to True.
>>> (0 < 0) == 0
True
This is equivalent to False == 0 which evaluates to True.
>>> 0 < (0 == 0)
True
Equivalent to 0 < True which, as above, evaluates to True.

Looking at the disassembly (the bytes codes) it is obvious why 0 < 0 == 0 is False.
Here is an analysis of this expression:
>>>import dis
>>>def f():
... 0 < 0 == 0
>>>dis.dis(f)
2 0 LOAD_CONST 1 (0)
3 LOAD_CONST 1 (0)
6 DUP_TOP
7 ROT_THREE
8 COMPARE_OP 0 (<)
11 JUMP_IF_FALSE_OR_POP 23
14 LOAD_CONST 1 (0)
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
Notice lines 0-8: These lines check if 0 < 0 which obviously returns False onto the python stack.
Now notice line 11: JUMP_IF_FALSE_OR_POP 23
This means that if 0 < 0 returns False perform a jump to line 23.
Now, 0 < 0 is False, so the jump is taken, which leaves the stack with a False which is the return value for the whole expression 0 < 0 == 0, even though the == 0 part isn't even checked.
So, to conclude, the answer is like said in other answers to this question.
0 < 0 == 0 has a special meaning. The compiler evaluates this to two terms: 0 < 0 and 0 == 0. As with any complex boolean expressions with and between them, if the first fails then the second one isn't even checked.
Hopes this enlightens things up a bit, and I really hope that the method I used to analyse this unexpected behavior will encourage others to try the same in the future.

As other's mentioned x comparison_operator y comparison_operator z is syntactical sugar for (x comparison_operator y) and (y comparison_operator z) with the bonus that y is only evaluated once.
So your expression 0 < 0 == 0 is really (0 < 0) and (0 == 0), which evaluates to False and True which is just False.

maybe this excerpt from the docs can help:
These are the so-called “rich
comparison” methods, and are called
for comparison operators in preference
to __cmp__() below. The correspondence
between operator symbols and method
names is as follows: x<y calls
x.__lt__(y), x<=y calls x.__le__(y),
x==y calls x.__eq__(y), x!=y and x<>y
call x.__ne__(y), x>y calls
x.__gt__(y), and x>=y calls
x.__ge__(y).
A rich comparison method may return
the singleton NotImplemented if it
does not implement the operation for a
given pair of arguments. By
convention, False and True are
returned for a successful comparison.
However, these methods can return any
value, so if the comparison operator
is used in a Boolean context (e.g., in
the condition of an if statement),
Python will call bool() on the value
to determine if the result is true or
false.
There are no implied relationships
among the comparison operators. The
truth of x==y does not imply that x!=y
is false. Accordingly, when defining
__eq__(), one should also define __ne__() so that the operators will behave as expected. See the paragraph
on __hash__() for some important notes
on creating hashable objects which
support custom comparison operations
and are usable as dictionary keys.
There are no swapped-argument versions
of these methods (to be used when the
left argument does not support the
operation but the right argument
does); rather, __lt__() and __gt__()
are each other’s reflection, __le__()
and __ge__() are each other’s
reflection, and __eq__() and __ne__()
are their own reflection.
Arguments to rich comparison methods
are never coerced.
These were comparisons but since you are chaining comparisons you should know that:
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.

Here it is, in all its glory.
>>> class showme(object):
... def __init__(self, name, value):
... self.name, self.value = name, value
... def __repr__(self):
... return "<showme %s:%s>" % (self.name, self.value)
... def __cmp__(self, other):
... print "cmp(%r, %r)" % (self, other)
... if type(other) == showme:
... return cmp(self.value, other.value)
... else:
... return cmp(self.value, other)
...
>>> showme(1,0) < showme(2,0) == showme(3,0)
cmp(<showme 1:0>, <showme 2:0>)
False
>>> (showme(1,0) < showme(2,0)) == showme(3,0)
cmp(<showme 1:0>, <showme 2:0>)
cmp(<showme 3:0>, False)
True
>>> showme(1,0) < (showme(2,0) == showme(3,0))
cmp(<showme 2:0>, <showme 3:0>)
cmp(<showme 1:0>, True)
True
>>>

I'm thinking Python is doing it's weird between magic. Same as 1 < 2 < 3 means 2 is between 1 and 3.
In this case, I think it's doing [middle 0] is greater than [left 0] and equal to [right 0]. Middle 0 is not greater than left 0, so it evaluates to false.

Related

python 3: Why do I get conflicting results when comparing to bool [duplicate]

Looking into Queue.py in Python 2.6, I found this construct that I found a bit strange:
def full(self):
"""Return True if the queue is full, False otherwise
(not reliable!)."""
self.mutex.acquire()
n = 0 < self.maxsize == self._qsize()
self.mutex.release()
return n
If maxsize is 0 the queue is never full.
My question is how does it work for this case? How 0 < 0 == 0 is considered False?
>>> 0 < 0 == 0
False
>>> (0) < (0 == 0)
True
>>> (0 < 0) == 0
True
>>> 0 < (0 == 0)
True
I believe Python has special case handling for sequences of relational operators to make range comparisons easy to express. It's much nicer to be able to say 0 < x <= 5 than to say (0 < x) and (x <= 5).
These are called chained comparisons. And that's a link to the documentation for them.
With the other cases you talk about, the parentheses force one relational operator to be applied before the other, and so they are no longer chained comparisons. And since True and False have values as integers you get the answers you do out of the parenthesized versions.
Because
(0 < 0) and (0 == 0)
is False. You can chain together comparison operators and they are automatically expanded out into the pairwise comparisons.
EDIT -- clarification about True and False in Python
In Python True and False are just instances of bool, which is a subclass of int. In other words, True really is just 1.
The point of this is that you can use the result of a boolean comparison exactly like an integer. This leads to confusing things like
>>> (1==1)+(1==1)
2
>>> (2<1)<1
True
But these will only happen if you parenthesise the comparisons so that they are evaluated first. Otherwise Python will expand out the comparison operators.
The strange behavior your experiencing comes from pythons ability to chain conditions. Since it finds 0 is not less than 0, it decides the entire expression evaluates to false. As soon as you break this apart into seperate conditions, you're changing the functionality. It initially is essentially testing that a < b && b == c for your original statement of a < b == c.
Another example:
>>> 1 < 5 < 3
False
>>> (1 < 5) < 3
True
>>> 0 < 0 == 0
False
This is a chained comparison. It returns true if each pairwise comparison in turn is true. It is the equivalent to (0 < 0) and (0 == 0)
>>> (0) < (0 == 0)
True
This is equivalent to 0 < True which evaluates to True.
>>> (0 < 0) == 0
True
This is equivalent to False == 0 which evaluates to True.
>>> 0 < (0 == 0)
True
Equivalent to 0 < True which, as above, evaluates to True.
Looking at the disassembly (the bytes codes) it is obvious why 0 < 0 == 0 is False.
Here is an analysis of this expression:
>>>import dis
>>>def f():
... 0 < 0 == 0
>>>dis.dis(f)
2 0 LOAD_CONST 1 (0)
3 LOAD_CONST 1 (0)
6 DUP_TOP
7 ROT_THREE
8 COMPARE_OP 0 (<)
11 JUMP_IF_FALSE_OR_POP 23
14 LOAD_CONST 1 (0)
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
Notice lines 0-8: These lines check if 0 < 0 which obviously returns False onto the python stack.
Now notice line 11: JUMP_IF_FALSE_OR_POP 23
This means that if 0 < 0 returns False perform a jump to line 23.
Now, 0 < 0 is False, so the jump is taken, which leaves the stack with a False which is the return value for the whole expression 0 < 0 == 0, even though the == 0 part isn't even checked.
So, to conclude, the answer is like said in other answers to this question.
0 < 0 == 0 has a special meaning. The compiler evaluates this to two terms: 0 < 0 and 0 == 0. As with any complex boolean expressions with and between them, if the first fails then the second one isn't even checked.
Hopes this enlightens things up a bit, and I really hope that the method I used to analyse this unexpected behavior will encourage others to try the same in the future.
As other's mentioned x comparison_operator y comparison_operator z is syntactical sugar for (x comparison_operator y) and (y comparison_operator z) with the bonus that y is only evaluated once.
So your expression 0 < 0 == 0 is really (0 < 0) and (0 == 0), which evaluates to False and True which is just False.
maybe this excerpt from the docs can help:
These are the so-called “rich
comparison” methods, and are called
for comparison operators in preference
to __cmp__() below. The correspondence
between operator symbols and method
names is as follows: x<y calls
x.__lt__(y), x<=y calls x.__le__(y),
x==y calls x.__eq__(y), x!=y and x<>y
call x.__ne__(y), x>y calls
x.__gt__(y), and x>=y calls
x.__ge__(y).
A rich comparison method may return
the singleton NotImplemented if it
does not implement the operation for a
given pair of arguments. By
convention, False and True are
returned for a successful comparison.
However, these methods can return any
value, so if the comparison operator
is used in a Boolean context (e.g., in
the condition of an if statement),
Python will call bool() on the value
to determine if the result is true or
false.
There are no implied relationships
among the comparison operators. The
truth of x==y does not imply that x!=y
is false. Accordingly, when defining
__eq__(), one should also define __ne__() so that the operators will behave as expected. See the paragraph
on __hash__() for some important notes
on creating hashable objects which
support custom comparison operations
and are usable as dictionary keys.
There are no swapped-argument versions
of these methods (to be used when the
left argument does not support the
operation but the right argument
does); rather, __lt__() and __gt__()
are each other’s reflection, __le__()
and __ge__() are each other’s
reflection, and __eq__() and __ne__()
are their own reflection.
Arguments to rich comparison methods
are never coerced.
These were comparisons but since you are chaining comparisons you should know that:
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.
Here it is, in all its glory.
>>> class showme(object):
... def __init__(self, name, value):
... self.name, self.value = name, value
... def __repr__(self):
... return "<showme %s:%s>" % (self.name, self.value)
... def __cmp__(self, other):
... print "cmp(%r, %r)" % (self, other)
... if type(other) == showme:
... return cmp(self.value, other.value)
... else:
... return cmp(self.value, other)
...
>>> showme(1,0) < showme(2,0) == showme(3,0)
cmp(<showme 1:0>, <showme 2:0>)
False
>>> (showme(1,0) < showme(2,0)) == showme(3,0)
cmp(<showme 1:0>, <showme 2:0>)
cmp(<showme 3:0>, False)
True
>>> showme(1,0) < (showme(2,0) == showme(3,0))
cmp(<showme 2:0>, <showme 3:0>)
cmp(<showme 1:0>, True)
True
>>>
I'm thinking Python is doing it's weird between magic. Same as 1 < 2 < 3 means 2 is between 1 and 3.
In this case, I think it's doing [middle 0] is greater than [left 0] and equal to [right 0]. Middle 0 is not greater than left 0, so it evaluates to false.

Python - 'if not'

The following codes
multiples = []
for i in range(1,1000):
if i % 3 == 0 or i % 5 == 0:
multiples.append(i)
addition = sum(multiples)
print addition
and
print(sum([i for i in range(1, 1000) if not (i%3 and i%5)]))
do the same thing.
Now how does the if not part compute in the second code?
What I'm saying is, in the first code the i % 3 == 0 or i % 5 == 0 had to be exclusively stated whereas the same thing is achieved on the second code without the == 0.
Using De Morgan's laws:
i % 3 == 0 or i % 5 == 0
is the same as:
not (i % 3 != 0 and i % 5 != 0)
And in python, when converting a number to a boolean, any non-zero value becomes True.
So instead of doing i % 3 != 0 in the if, you can just use i % 3, because if it's 0 it'll be False and if it's non-zero it'll be True.
Here's python's truth table: https://docs.python.org/3.6/library/stdtypes.html#truth
P.S. sum() can take a generator as an argument, so you can actually just do:
sum(i for i in range(1, 1000) if not (i%3 and i%5))
and and or are boolean operators, not logical & and |. So writing
a == 0 or b == 0
is the same as writing
not a or not b
So they do the same thing
As a conclusion, the best way is to avoid negations in your conditions, don't create extra list comprehension but use generator comprehension instead, and I wouldn't mind testing against zero instead of using not since they're integers after all. I would do this:
print(sum(i for i in range(1, 1000) if i%3==0 or i%5==0))
This is an effect of the way Python converts integers to booleans. The result of i % 3 is 0, 1, or 2, depending on the value of i. In Python 3.x (and by default also in Python 2.x, unless you've reassigned them) all non-zero integers evaluate to False and zero evaluates to True.
Thus if not (i % 3) will evaluate (i % 3) to an integer 0, 1, or 2, and then not will convert these to False, True, True.
In Python 0 is usual interpreted as false. So in the first code block i % 3 == 0 is effectively equivalent to not i % 3. Examples from command line:
>>> 6 % 3 == 0
True
>>> not 6 % 3
True
>>> 7 % 3 == 0
False
>>> not 7 % 3
False
This shortcut of 0 === false appears in many languages and can be a bit confusing if you're not used to it.
In Python every object has a boolean value.
In the case of integers any of them is True except 0.
So in your case when number i can be divided by 3 the expression i % 3 will be 0. But we know that 0 is False. The not operator will negate this expression so not False will become True.
The same holds for not i % 5 as well.
Hope it helps.
Any object can be tested for truth value, for use in an if or while condition or as operand of the Boolean operations below. The following values are considered false:
None
False
zero of any numeric type, for example, 0, 0L, 0.0, 0j.
any empty sequence, for example, '', (), [].
any empty mapping, for example, {}.
instances of user-defined classes, if the class defines a nonzero() or len() method, when that method returns the integer zero or bool value False. [1]
All other values are considered true — so objects of many types are always true.
See also:
https://docs.python.org/2/library/stdtypes.html
see, i % 3 == 0 return true if the condition satisfy. Now, in python suppose:
i = 6
6 % 3 = 0 and 0==0 is true
There fore the result will be true.
In the second case
6 % 3 = 0.
Now, in boolean 0 means false. So by using not it means not false, i.e, true.

Associativity of comparison operators in Python

What is the associativity of comparison operators in Python? It is straightforward for three comparisons, but for more than that, I'm not sure how it does it. They don't seem to be right- or left-associative.
For example:
>>> 7410 >= 8690 <= -4538 < 9319 > -7092
False
>>> (((7410 >= 8690) <= -4538) < 9319) > -7092
True
So, not left-associative.
>>> 81037572 > -2025 < -4722 < 6493
False
>>> (81037572 > (-2025 < (-4722 < 6493)))
True
So it's not right-associative either.
I have seen some places that they are 'chained', but how does that work with four or more comparisons?
Chained comparisons are expanded with and, so:
a <= b <= c
becomes:
a <= b and b <= c
(b is only evaluated once, though). This is explained in the language reference on comparisons.
Note that lazy evaluation means that if a > b, the result is False and b is never compared to c.
Your versions with parentheses are completely different; a <= (b <= c) will evaluate b <= c then compare a to the result of that, and isn't involved at all, so it's not meaningful to compare the results to determine associativity.
python short-circits boolean tests from left to right:
7410>=8690<=-4538<9319>-7092 -> False
7410>=8690 is False. that's it. the rest of the tests is not preformed.
note that
True == 1
False == 0
are both True and apply when you compare the booleans with integers. so when you surround the statement with brackets you force python to do all the tests; in detail:
(((7410>=8690)<=-4538)<9319)>-7092
False <=-4538
False <9319
True >-7092
True
You are making an error with types, when you write 81037572>-2025 then the system thinks of this as True or False and associates it with 1 and 0. It therefore then gives you a comparison with those binary numbers.

What is the meaning of True == True != False in Python and how to find out?

I actually needed xor for my solution, but while thinking on it, I started wondering about the question above. What is the meaning of True == True != False?
Looking at the documentation I suppose it's True == True and True != False, but I'd like a more general and certain approach. How do I quickly get readable form of bytecode for such code. Is there an easier way to find out than both bytecode and documentation?
It's called operator chaining. Whenever you have an expression like A op1 B op2 C with op1 and op2 comparisons it is "translated" to A op1 B and B op2 C.
(Actually it does evaluate B only once).
Note: comparisons operator include in, not in, is, is not! (e.g. a is b is not None means a is b and b is not None).
If you want to look at bytecode you can use the dis module:
In [1]: import dis
In [2]: dis.dis(lambda: True == True != False)
1 0 LOAD_CONST 1 (True)
3 LOAD_CONST 1 (True)
6 DUP_TOP
7 ROT_THREE
8 COMPARE_OP 2 (==)
11 JUMP_IF_FALSE_OR_POP 21
14 LOAD_CONST 2 (False)
17 COMPARE_OP 3 (!=)
20 RETURN_VALUE
>> 21 ROT_TWO
22 POP_TOP
23 RETURN_VALUE
If you read at the bytecode you can understand that it performs operator chaining.
Given that the expression is True == True != False, which is "interpreted" as True == True and True != False it first loads the two True constants for the first operator via the LOAD_CONST bytecode. The DUP_TOP duplicates the top of the stack(this avoids re-evaluating True for the second comparison).
It performs the first comparison(COMPARE_OP) if it is false it just to the bytecode 21, otherwise it pops the top of the stack(JUMP_IF_FALSE_OR_POP). It then performs the second comparison.
To answer your general question the fastest way to find out about some feature of python is to use the quicksearch page of the documentation.
I'd also suggest to read Python's tutorial for a general introduction to the language.
I'd like to add that, since python provides an interactive environment, it is often easier to understand how some code works writing it in the interpreter and watching the results. Almost all buil-in types have the documentation available via docstrings, so doing help(some_object) should give you a lot of information.
In particular IPython provides an enhanced interactive interpreter with more user-friendly help messages/error formatting etc.)
In most languages, a == b != c parses as (a == b) != c.
Therefore, what you would expect is that True == True != False would be the same as (True == True) != False, which evaluates to True != False, which evaluates to True.
Python has a different meaning, as can be witnessed here:
>>> True != False != False
False
>>> (True != False) != False
True
In Python, a == b != c is equivalent to (a == b) and (b != c).
This means that True == True != False is equivalent to (True == True) and (True != False), which evaluates to True and True, which evaluates to True.
Coincidentally, both meanings (Python's and that of other languages) give the same result here, but one should be cautious.

Python statement giving unexpected answer [duplicate]

This question already has answers here:
Why does the expression 0 < 0 == 0 return False in Python?
(9 answers)
Closed 5 years ago.
print 'a' in 'ab'
prints True, while
print 'a' in 'ab' == True
prints False.
Any guess why?
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.
>>> '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.
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.

Categories

Resources