Why pyc.exec, and how scoping works
Newcomers almost always ask the same question: why do I have to wrap my code in
pyc.exec("...") instead of just writing it normally? The answer is a
consequence of when Python compiles code, and it is worth understanding because it
also tells you exactly when you don’t need pyc.exec.
The AST is fixed at compile time
A module’s abstract syntax tree is built once, when the module is compiled and imported — and Pyccolo’s instrumentation is an AST rewrite (see The model: events and handlers). When your script first starts running, the module containing your tracer is already compiled, and the tracer was not active at that moment. So any ordinary, unquoted Python written in the same file as the tracer definition has no instrumentation woven into it — there was no active tracer to weave it in.
To instrument code that lives in that file, you “quote” it as a string (or an AST)
and hand it to pyc.exec or pyc.eval. These parse it, run it through the
instrumentation pipeline, and execute the result — this time with your tracer
active:
import pyccolo as pyc
class HelloTracer(pyc.BaseTracer):
@pyc.before_stmt
def handle_stmt(self, *_, **__):
print("Hello, world!")
with HelloTracer:
pyc.exec("for _ in range(10): pass") # instrumented; prints 11 times
pyc.exec returns a namespace
pyc.exec runs its code in a sandbox and returns the resulting namespace as a
dict, so you can inspect what the executed code produced:
env = pyc.exec("x = 42")
assert env["x"] == 42
pyc.eval is the expression-valued counterpart, returning the value of a single
expression. Both compile under a synthetic sandbox filename so their frames are
recognizable; if you need those frames visible in tracebacks, see the
traceback-visibility helpers in Utilities & globals.
When you don’t need pyc.exec
Two important cases run instrumented code without any quoting:
``sys`` events. Handlers for
call,line,return_,exception, andopcoderide on Python’s built-in tracing machinery, not on an AST rewrite. Nothing needs to be recompiled, so ordinary in-file code triggers them directly. See Tracing real programs.Imported modules. Code imported inside a tracing context can be instrumented at import time, because Pyccolo controls the import and can rewrite the module as it loads — provided the module opts in via
should_instrument_file(). See Tracing real programs.
Reaching into the caller’s scope
Handlers receive the executing frame, and sometimes need to write a value back
into it — for example to set up a guard variable or bind a synthetic name.
pyc.set_frame_local does this safely across the frame’s local and global
namespaces, rather than mutating frame.f_locals directly (which does not
reliably propagate). It is part of the utilities reference,
alongside the sandbox-filename constants used to identify pyc.exec frames. For
the full set of run-and-compile entry points, see BaseTracer.