CPython 2 had PYTHONSTARTUP for interactive sessions, but no general hook at the boundary zippy needed: after interpreter startup finalized argv and sys.path, before user code, across execution modes. site.py runs early, and then PySys_SetArgv—C code, while setting sys.argv—prepends the current directory to sys.path after site import. Whatever site.py arranged gets amended behind its back.
zippy, my tool for building CPython 2 into a single static binary, needed the last word on sys.path. A trace hook supplied a one-shot callback at the boundary it needed.
The build overwrites CPython’s Lib/site.py with a few lines that swap the site module for zippy’s own, and that replacement ends by arming sys.settrace:
# HACKZILLA!
# python will prepend the current directory to sys.path, from C code
# (PySys_SetArgv), while setting sys.argv... but this happens AFTER
# site.py is imported!
def tracer(frame, event, args):
if frame.f_globals.get('__name__') == '__main__':
sys.settrace(tracer_orig)
syspath = normalize_syspath(sys.path)
sys.path[:] = syspath
if building:
m_file = frame.f_globals.get('__file__') or str()
if m_file.strip('co').endswith('compileall.py'):
raise SystemExit(0)
tracer_orig = sys.gettrace()
sys.settrace(tracer)The tracer runs for every call during the remainder of startup and does nothing until it sees the first frame whose __name__ is '__main__'. Zippy treats that as the post-startup boundary: argv is set and the cwd entry is in place. It then restores the previous trace function; if that was None, the extra tracing cost ends there. It de-dupes sys.path and strips the cwd spellings the interpreter prepended after site import, collapsing them to the single canonical '' entry.
Restoring the old hook does not replay the events it missed. This tracer never calls tracer_orig, and it returns no local trace function for the first __main__ frame. In CPython 2’s trace dispatcher, that leaves the frame without the old hook’s line tracing; later calls can reach the restored global hook. This was a startup interception, not transparent composition with a debugger already tracing the process.
The same tracer moonlights during zippy’s own build. When the frozen interpreter runs compileall.py—note the strip('co'), which also matches .pyc and .pyo—it raises SystemExit(0), ending the compile pass cleanly. The same build forces Py_DontWriteBytecodeFlag to 1 at the C-default level; a bytecode-free interpreter is the posture, and the compileall abort completes it.
The comment block earns a longer quote. After explaining the PySys_SetArgv ordering it keeps going:
uwsgi could avoid this by calling PySys_SetArgvEx, but this wouldn’t be consistent with vanilla python, and uwsgi has it’s own set of problems, eg. setting sys.executable to realpath(argv[0]) instead of using the original symlink path, like python… “arg”!!!
The gotcha is inherent: a trace function observes every call event until it disarms, so this only stays cheap because the window between site import and the first __main__ frame is short and the tracer’s fast path is one dict lookup. Arm it and forget to disarm it and you are profiling your whole program by accident.