IPython and Jupyter

In a notebook you don’t want to wrap every cell in pyccolo.exec(). Pyccolo ships an IPython extension that instruments each cell automatically: load it, then register one or more tracers, and every subsequent cell is traced as if it had been run through pyc.exec — including notebook niceties like Out[N] and composition with other cell-level tools such as ipyflow.

Load the extension and register a tracer

In [1]: %load_ext pyccolo

In [2]: import pyccolo as pyc
   ...: class HelloTracer(pyc.BaseTracer):
   ...:     @pyc.before_stmt
   ...:     def handle_stmt(self, *_, **__):
   ...:         print("Hello, world!")

In [3]: %pyccolo register HelloTracer

In [4]: for _ in range(3): pass
Hello, world!
Hello, world!
Hello, world!
Hello, world!

%unload_ext pyccolo removes the instrumentation and restores the shell to exactly how it was.

The four ways to name a tracer

%pyccolo register and its programmatic twin pyccolo.register_ipython_tracer() accept the tracer in four spellings, so you can register from a cell, a variable, or a library:

pyc.register_ipython_tracer(HelloTracer)                  # a class
pyc.register_ipython_tracer(HelloTracer.instance())       # an instance
pyc.register_ipython_tracer("HelloTracer")                # a name in the user namespace
pyc.register_ipython_tracer("mypkg.tracers.HelloTracer")  # a dotted path

The extension must already be loaded or register_ipython_tracer raises. The companion subcommands are %pyccolo deregister <tracer> (or deregister all) and %pyccolo list, plus pyccolo.deregister_ipython_tracer() and pyccolo.registered_ipython_tracers().

Registration takes effect on the next cell: by the time your register call runs, the current cell has already been rewritten.

Registration order is canonical

The first tracer registered comes first in the tracer stack: its syntax augmenter rewrites the source outermost, and its handlers see each event first.

pyc.register_ipython_tracer(A)
pyc.register_ipython_tracer(B)
# A's augmenter runs before B's; A's handlers run before B's

Composing with ipyflow

ipyflow drives Pyccolo’s cell-tracing driver directly instead of layering a second set of transformers on the shell, so %load_ext pyccolo and %load_ext ipyflow compose in either order and a cell is instrumented exactly once. %flow register_tracer and pyc.register_ipython_tracer write to the same registry, and ipyflow’s own tracers sort after yours. %unload_ext ipyflow hands the driver back, leaving your tracers running under Pyccolo alone. The handoff primitives — pyccolo.take_over_ipython_driver() and pyccolo.release_ipython_driver() — are what make this seamless.

How it works

Pyccolo installs one input transformer (which applies each tracer’s syntax augmenters and builds the cell’s AstRewriter) and one AST transformer (which runs that rewriter). It learns the cell’s filename by wrapping shell.compile.cache — the caching compiler that mints it, called between the two transforms — and adds it to each tracer’s tracing-enabled files so the file filter admits the cell. Tracing is resident: tracers are entered once and merely enabled/disabled around each cell, rather than pushed and popped every time.

Two things worth knowing when you write a tracer for the notebook

Define tracers in a module, not a cell, when you can. A tracer defined in a cell has its handler’s code object attributed to that cell’s filename, so co-resident tracers that use sys.settrace may follow execution into your handler.

You don’t lose Out[N]. Instrumenting a statement wraps it in an if, so a cell body no longer ends in a bare expression statement — which would normally make IPython’s last_expr interactivity suppress the output. Pyccolo detects this and weaves in an after_module_stmt handler that preserves the trailing expression’s value, so Out[N] behaves as usual. Nothing is required of you.

JupyterLite

The extension imports only from IPython.core and never touches shell.kernel, so it runs unmodified under JupyterLite / Pyodide, where ipykernel is absent. The in-browser demo is exactly this path — Pyccolo tracing cells, entirely client-side.