Why does Python have variable hoisting like Javascript?

That depends. Loading a local variable in non-optimized code (e.g. module, class body, or exec) uses the LOAD_NAME opcode. For example:

>>> dis.dis(compile('x = 0; x', '', 'exec'))
  1           0 LOAD_CONST               0 (0)
              2 STORE_NAME               0 (x)
              4 LOAD_NAME                0 (x)
              6 POP_TOP
              8 LOAD_CONST               1 (None)
             10 RETURN_VALUE

LOAD_NAME falls back on globals and builtins, which makes it easy to temporarily shadow a global or builtin name. For example:

>>> exec(r'''
... abs = 'temporary shadow'
... print(abs)
... del abs
... print(abs)
... ''')
temporary shadow
<built-in function abs>

This won’t work in optimized code (e.g. a function created by the def statement), which uses the LOAD_FAST opcode to load a local variable that’s in the fast locals array, which does not fall back on globals and builtins. For example:

>>> def f():
...     abs = 'temp'
...     del abs
...     abs
... 
>>> dis.dis(f)
  2           0 LOAD_CONST               1 ('temp')
              2 STORE_FAST               0 (abs)

  3           4 DELETE_FAST              0 (abs)

  4           6 LOAD_FAST                0 (abs)
              8 POP_TOP
             10 LOAD_CONST               0 (None)
             12 RETURN_VALUE
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in f
UnboundLocalError: local variable 'abs' referenced before assignment

In optimized code, one has to explicitly reset back to a known global or builtin reference, such as abs = builtins.abs.

1 Like