Why in Python does "0, 0 == (0, 0)" equal "(0, False)"?

Question:

In Python (I checked only with Python 3.6 but I believe it should hold for many of the previous versions as well):

(0, 0) == 0, 0   # results in a two element tuple: (False, 0)
0, 0 == (0, 0)   # results in a two element tuple: (0, False)
(0, 0) == (0, 0) # results in a boolean True

But:

a = 0, 0
b = (0, 0)
a == b # results in a boolean True

Why does the result differ between the two approaches? Does the equality operator handle tuples differently?

Asked By: Piotr Zakrzewski

||

Answers:

The first two expressions both parse as tuples:

  1. (0, 0) == 0 (which is False), followed by 0
  2. 0, followed by 0 == (0, 0) (which is still False that way around).

The expressions are split that way because of the relative precedence of the comma separator compared to the equality operator: Python sees a tuple containing two expressions, one of which happens to be an equality test, instead of an equality test between two tuples.

But in your second set of statements, a = 0, 0 cannot be a tuple. A tuple is a collection of values, and unlike an equality test, assignment has no value in Python. An assignment is not an expression, but a statement; it does not have a value that can be included into a tuple or any other surrounding expression. If you tried something like (a = 0), 0 in order to force interpretation as a tuple, you would get a syntax error. That leaves the assignment of a tuple to a variable – which could be made more explicit by writing it a = (0, 0) – as the only valid interpretation of a = 0, 0.

So even without the parentheses on the assignment to a, both it and b get assigned the value (0,0), so a == b is therefore True.

Answered By: Mark Reed

In the first one Python is making a tuple of two things:

  1. The expression (0, 0) == 0, which evaluates to False
  2. The constant 0

In the second one it’s the other way around.

Answered By: kindall

What you see in all 3 instances is a consequence of the grammar specification of the language, and how tokens encountered in the source code are parsed to generate the parse tree.

Taking a look at this low level code should help you understand what happens under the hood. We can take these python statements, convert them into byte code and then decompile them using the dis module:

Case 1: (0, 0) == 0, 0

>>> dis.dis(compile("(0, 0) == 0, 0", '', 'exec'))
  1           0 LOAD_CONST               2 ((0, 0))
              3 LOAD_CONST               0 (0)
              6 COMPARE_OP               2 (==)
              9 LOAD_CONST               0 (0)
             12 BUILD_TUPLE              2
             15 POP_TOP
             16 LOAD_CONST               1 (None)
             19 RETURN_VALUE

(0, 0) is first compared to 0 first and evaluated to False. A tuple is then constructed with this result and last 0, so you get (False, 0).

Case 2: 0, 0 == (0, 0)

>>> dis.dis(compile("0, 0 == (0, 0)", '', 'exec'))
  1           0 LOAD_CONST               0 (0)
              3 LOAD_CONST               0 (0)
              6 LOAD_CONST               2 ((0, 0))
              9 COMPARE_OP               2 (==)
             12 BUILD_TUPLE              2
             15 POP_TOP
             16 LOAD_CONST               1 (None)
             19 RETURN_VALUE

A tuple is constructed with 0 as the first element. For the second element, the same check is done as in the first case and evaluated to False, so you get (0, False).

Case 3: (0, 0) == (0, 0)

>>> dis.dis(compile("(0, 0) == (0, 0)", '', 'exec'))
  1           0 LOAD_CONST               2 ((0, 0))
              3 LOAD_CONST               3 ((0, 0))
              6 COMPARE_OP               2 (==)
              9 POP_TOP
             10 LOAD_CONST               1 (None)
             13 RETURN_VALUE

Here, as you see, you’re just comparing those two (0, 0) tuples and returning True.

Answered By: cs95

Adding a couple of parentheses around the order in which actions are performed might help you understand the results better:

# Build two element tuple comprising of 
# (0, 0) == 0 result and 0
>>> ((0, 0) == 0), 0
(False, 0)

# Build two element tuple comprising of
# 0 and result of (0, 0) == 0 
>>> 0, (0 == (0, 0))
(0, False)

# Create two tuples with elements (0, 0) 
# and compare them
>>> (0, 0) == (0, 0) 
True

The comma is used to to separate expressions (using parentheses we can force different behavior, of course). When viewing the snippets you listed, the comma , will separate it and define what expressions will get evaluated:

(0, 0) == 0 ,   0
#-----------|------
  expr 1      expr2

The tuple (0, 0) can also be broken down in a similar way. The comma separates two expressions comprising of the literals 0.

Another way to explain the problem: You’re probably familiar with dictionary literals

{ "a": 1, "b": 2, "c": 3 }

and array literals

[ "a", "b", "c" ]

and tuple literals

( 1, 2, 3 )

but what you don’t realize is that, unlike dictionary and array literals, the parentheses you usually see around a tuple literal are not part of the literal syntax. The literal syntax for tuples is just a sequence of expressions separated by commas:

1, 2, 3

(an “exprlist” in the language of the formal grammar for Python).

Now, what do you expect the array literal

[ 0, 0 == (0, 0) ]

to evaluate to? That probably looks a lot more like it should be the same as

[ 0, (0 == (0, 0)) ]

which of course evaluates to [0, False]. Similarly, with an explicitly parenthesized tuple literal

( 0, 0 == (0, 0) )

it’s not surprising to get (0, False). But the parentheses are optional;

0, 0 == (0, 0)

is the same thing. And that’s why you get (0, False).


If you’re wondering why the parentheses around a tuple literal are optional, it is largely because it would be annoying to have to write destructuring assignments that way:

(a, b) = (c, d) # meh
a, b = c, d     # better
Answered By: zwol

look at this example:

r = [1,0,1,0,1,1,0,0,0,1]
print(r==0,0,r,1,0)
print(r==r,0,1,0,1,0)

then result:

False 0 [1, 0, 1, 0, 1, 1, 0, 0, 0, 1] 1 0
True 0 1 0 1 0

then comparison just does to the first number(0 and r) in the example.

Answered By: Sadi

I had a similar question. I am not a computer scientist, I am either a software engineer or a computer programmer. So I asked the python interpreter and this is what I found, empirically.

>>> t1 = ()
>>> "True" if t1 else "False"
'False'
>>> t1 = (False)     # That's because t1 is not a tuple!
>>> "True" if t1 else "False"
'False'
>>> t1 = (False,)     # t1 is a tuple.  So , is an operator as mentioned above
>>> "True" if t1 else "False"
'True'
>>> t1 = (False, 1)
>>> "True" if t1 else "False"
'True'
>>> t1 = (False, False)
>>> "True" if t1 else "False"
'True'
>>> type(False,)
<class 'bool'>
>>> type((False,))
<class 'tuple'>
>>> type(False)
<class 'bool'>
>>> type((False))
<class 'bool'>
>>>

I did a lot of testing and the only tuple I found that evaluate to False is the empty tuple.

I also learned something in this exercise. A lot of rookies use the idiom:

if BOOLEAN_EXPRESSION == False:

instead of

if not BOOLEAN_EXPRESSION:

"Why is this a bad thing to do?", they ask me. Now, I have a good answer:

>>> (False,) == False
False
>>> t1=(False,)
>>> "True" if t1 else "False"
'True'
>>> t1 == False
False
>>>
>>> t1=(False,)
>>> "True" if t1 else "False"
'True'
>>> t1 == False
False
>>> t1 is False
False
>>> not t1 is False
True
>>> not ( t1 is False )
True
>>>
>>> "True" if t1 else "False"
'True'
>>> "True" if not t1 else "False"
'False'
>>> "True" if t1 == True else "False"
'False'
>>>


So even though (False,) evaluates to False, it is not False.

I would like to thank you for asking this question to my attention. It’s a great question.

Answered By: Jeff Silverman