When you really want to know what happens behind the scenes in the interpreter, you can use the dis module. In this case:
>>> def f1(): ... if a: ... b = 1 ... elif aa: ... b = 2 ... >>> def f2(): ... if a: ... b = 1 ... else: ... if aa: ... b = 2 ... >>> dis.dis(f1) 2 0 LOAD_GLOBAL 0 (a) 3 POP_JUMP_IF_FALSE 15 3 6 LOAD_CONST 1 (1) 9 STORE_FAST 0 (b) 12 JUMP_FORWARD 15 (to 30) 4 >> 15 LOAD_GLOBAL 1 (aa) 18 POP_JUMP_IF_FALSE 30 5 21 LOAD_CONST 2 (2) 24 STORE_FAST 0 (b) 27 JUMP_FORWARD 0 (to 30) >> 30 LOAD_CONST 0 (None) 33 RETURN_VALUE >>> dis.dis(f2) 2 0 LOAD_GLOBAL 0 (a) 3 POP_JUMP_IF_FALSE 15 3 6 LOAD_CONST 1 (1) 9 STORE_FAST 0 (b) 12 JUMP_FORWARD 15 (to 30) 5 >> 15 LOAD_GLOBAL 1 (aa) 18 POP_JUMP_IF_FALSE 30 6 21 LOAD_CONST 2 (2) 24 STORE_FAST 0 (b) 27 JUMP_FORWARD 0 (to 30) >> 30 LOAD_CONST 0 (None) 33 RETURN_VALUE
It seems that our two functions use the same bytecode - so they seem to be equivalent.
Caution, however, bytecode is a detail of the implementation of CPython. It does not say that all python implementations do the same behind the scenes. All that matters is that they have the same behavior. By working in logic, you can convince yourself that f1 and f2 should do the same thing, regardless of whether the underlying implementation refers to it as “syntactic sugar” or if something more complex happens.
source share