["python","syntax"]
Associativity of "in" in Python?

I'm making a Python parser, and this is really confusing me: >>> 1 in [] in 'a' False >>> (1 in []) in 'a' TypeError: 'in <string>' requires string as left operand, not bool >>> 1 in ([] in 'a') TypeError: 'in <string>' requires string as left operand, not list How exactly does in work in Python, with regards to associativity, etc.? Why do no two of these expressions behave the same way?
1 in [] in 'a' is evaluated as (1 in []) and ([] in 'a').¹ Since the first condition (1 in []) is False, the whole condition is evaluated as False; ([] in 'a') is never actually evaluated, so no error is raised. We can see how Python executes each statement using the dis module: >>> from dis import dis >>> dis("1 in [] in 'a'") 1 0 LOAD_CONST 0 (1) 2 BUILD_LIST 0 4 DUP_TOP 6 ROT_THREE 8 CONTAINS_OP 0 # `in` is the contains operator 10 JUMP_IF_FALSE_OR_POP 18 # skip to 18 if the first # comparison is false 12 LOAD_CONST 1 ('a') # 12-16 are never executed 14 CONTAINS_OP 0 # so no error here (14) 16 RETURN_VALUE >> 18 ROT_TWO 20 POP_TOP 22 RETURN_VALUE >>> dis("(1 in []) in 'a'") 1 0 LOAD_CONST 0 (1) 2 LOAD_CONST 1 (()) 4 CONTAINS_OP 0 # perform 1 in [] 6 LOAD_CONST 2 ('a') # now load 'a' 8 CONTAINS_OP 0 # check if result of (1 in []) is in 'a' # throws Error because (False in 'a') # is a TypeError 10 RETURN_VALUE >>> dis("1 in ([] in 'a')") 1 0 LOAD_CONST 0 (1) 2 BUILD_LIST 0 4 LOAD_CONST 1 ('a') 6 CONTAINS_OP 0 # perform ([] in 'a'), which is # incorrect, so it throws a TypeError 8 CONTAINS_OP 0 # if no Error then this would # check if 1 is in the result of ([] in 'a') 10 RETURN_VALUE Except that [] is only evaluated once. This doesn't matter in this example but if you (for example) replaced [] with a function that returned a list, that function would only be called once (at most). The documentation explains also this.