Chained conditional expressions in python

Posted on July, 12 2015

NOTE: Before you start reading this, read up on stack machines if you have not done so yet. In this post I'll be going over some python bytecode, knowing what a stack machine will help a lot!

False == False in [False]

A friend had recently showed this statement to me, and asked what it evaluates to. I replied 'False', of course, since False == False is True, and True is not in the list [False]. Until, of course, I threw it in the python interpreter.

>>> False == False in [False]
True

Now, now... what's this madness? Is python pulling a javascript? To answer this problem we will disassemble a python function into python bytecode and evaluate it line-by-line.

>>> def func():
...     # We'll name these constants as they pop up in the stack
...     #      A        B        C
...     return False == False in [False]
... 
>>> import dis
>>> dis.dis(func)
  2           0 LOAD_GLOBAL              0 (False)
              3 LOAD_GLOBAL              0 (False)
              6 DUP_TOP             
              7 ROT_THREE           
              8 COMPARE_OP               2 (==)
             11 JUMP_IF_FALSE_OR_POP    24
             14 LOAD_GLOBAL              0 (False)
             17 BUILD_LIST               1
             20 COMPARE_OP               6 (in)
             23 RETURN_VALUE        
        >>   24 ROT_TWO             
             25 POP_TOP             
             26 RETURN_VALUE

Lines 0-3

On lines 0-3, LOAD_GLOBAL is called twice to load A and B into the stack. Below is a representation of the stack, with the label/ID mnemonic above each stack element.

#        TOS (top-of-stack), TOS1
#        B                   A
stack = [False,              False]

Line 6

DUP_TOP duplicates the 'top' of the stack.

#        TOS  , TOS1
#        DUP.B  B      A
stack = [False, False, False]

Line 7

ROT_THREE rotates the top three elements in the stack.

#        TOS  , TOS1
#        B      A      DUP.B
stack = [False, False, False]

Line 8

The next instruction is COMPARE_OP(==), which compares if TOS == TOS1, pops TOS and TOS1 before pushing the result of TOS == TOS1.

#        TOS , TOS1
#        A==B, DUP.B
stack = [True, False]

Line 11

JUMP_IF_FALSE_OR_POP jumps if TOS == False, else it pops the stack and moves on

#        TOS
#        DUP.B
stack = [False]

Huh, won't you look at that? The result of False == False was thrown away completely, leaving only the duplicate of B in the stack. This tells me that my initial theory was completely wrong, python is not chaining these operations together at all!

Lines 14-17

Now, the next set of instructions (14-17) builds the list, by pushing False on the stack and using BUILD_LIST(size) to create the list.

#       TOS  , TOS1
#        C     B
stack = [List, False]

Line 20

COMPARE_OP(in) tests whether TOS1 is in list TOS, which is True.

#        TOS
stack = [True]

The line after that is RETURN_VALUE, which returns the value of TOS, which is what we get when we run func()

Analysis

From what we can see, python does not chain these operations together as expected. It actually doesn't chain the operations together at all.

Looking at what values are in the stack at each instruction, we can see that instead of keeping the result of A == B, it keeps what we named as B in the stack and tested whether or not B was in the list C. On line 11, JUMP_IF_FALSE_OR_POP was used. If A == B were to evaluate to False , it would end up jumping to line 24 and return False.

Conclusion

In short, what the bytecode tells us is that the statement we wrote as

A == B in [C] is interpreted as A == B and B in C.

This must mean that given a statement structure as A is B is C, the statement is expanded into A is B and B is C.