Search code examples
pythonread-eval-print-loopbuilt-incpython

Why does deleting a global variable named __builtins__ prevent only the REPL from accessing builtins?


I have a python script with the following contents:

# foo.py

__builtins__ = 3
del __builtins__

print(int)  # <- this still works

Curiously, executing this script with the -i flag prevents only the REPL from accessing builtins:

aran-fey@starlight ~> python3 -i foo.py 
<class 'int'>
>>> print(int)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'print' is not defined

How come the script can access builtins, but the REPL can't?


Solution

  • CPython doesn't look up __builtins__ every time it needs to do a built-in variable lookup. Each frame object has an f_builtins member holding its built-in variable dict, and built-in variable lookup goes through there.

    f_builtins is set on frame object creation. If a new frame has no parent frame (f_back), or a different global variable dict from its parent frame, then frame object initialization looks up __builtins__ to set f_builtins. (If the new frame shares a global dict with its parent frame, then it inherits its parent's f_builtins.) This is the only way __builtins__ is involved in built-in variable lookup. You can see the code that handles this in _PyFrame_New_NoTrack.

    When you delete __builtins__ inside a script, that doesn't affect f_builtins. The rest of the code executing in the script's stack frame still sees builtins. Once the script completes and -i drops you into interactive mode, every interactive command gets a new stack frame (with no parent), and the __builtins__ lookup is repeated. This is when the deleted __builtins__ finally matter.