Pyccolo: Declarative Instrumentation for Python
Pyccolo (pronounced like "piccolo") is a library for declarative instrumentation in Python, allowing users to specify *what* instrumentation to perform rather than *how* to implement it. It aims for ergonomics, composability, and portability across various Python versions. It achieves this by embedding instrumentation at the source code level. The library is actively maintained, with the current version being 0.0.85, and supports Python versions from 3.6 up to 3.14 (since v0.0.73).
Common errors
-
ModuleNotFoundError: No module named 'pyccolo'
cause The `pyccolo` library has not been installed or is not accessible in the current Python environment.fixRun `pip install pyccolo` in your terminal to install the library. -
TypeError: 'NoneType' object is not callable (or similar error related to uninstrumented code)
cause An imported module or file was expected to be instrumented by a `pyccolo` tracer, but its code was executed without the tracer's handlers being applied.fixEnsure your custom `BaseTracer` implementation's `should_instrument_file` method correctly identifies and allows the target file(s) to be instrumented. Remember that instrumentation is opt-in for imports. -
SyntaxError: invalid syntax (when using optional chaining '?.')
cause Attempting to use new Python syntax features (like the optional chaining `?.`) that are either not supported by your Python version or require a specific `pyccolo` syntax augmentation tracer that isn't active.fixVerify that your Python interpreter is version 3.8 or higher. If the syntax is a `pyccolo` augmentation, ensure the corresponding tracer (e.g., `pyccolo.examples.OptionalChainer`) is activated. -
Unexpected expression value after handler execution (handler appeared to do nothing)
cause A handler was intended to override an expression's return value with `None`, but it implicitly returned `None` causing the original value to be used instead.fixIf the intention is to explicitly set the expression's value to `None` from a handler, return `pyccolo.Null` instead of `None`.
Warnings
- gotcha Pyccolo's instrumentation for imported modules is opt-in, not automatic. If you expect a module imported within a tracing context to be instrumented, you must explicitly enable it.
- gotcha When using handlers that can override expression return values (e.g., `before_attribute_load`), returning `None` means 'no override'. If you intend to explicitly override a value with `None`, you must return `pyccolo.Null`.
- gotcha Advanced syntax augmentation features, such as optional chaining (`?.`), are only supported on Python 3.8 and newer. Using them on older Python versions or without the specific tracer enabled will result in a `SyntaxError`.
- gotcha While Pyccolo is designed to be composable with existing `sys.settrace` functions, complex interactions can still occur. If you observe unexpected behavior with other tracing tools, it might be due to subtle conflicts.
Install
-
pip install pyccolo
Imports
- BaseTracer
from pyccolo import BaseTracer
- before_stmt
from pyccolo import before_stmt
- Null
from pyccolo import Null
Quickstart
import pyccolo as pyc
class MyTracer(pyc.BaseTracer):
def should_instrument_file(self, filename: str) -> bool:
# Only instrument files ending with 'my_script.py'
# For broader instrumentation, adjust this logic.
return 'my_script.py' in filename
@pyc.before_stmt
def on_before_statement(self, ret, node, frame, event):
print(f"Executing statement: {node.lineno}: {node.__class__.__name__}")
# Example usage with a dummy script content
script_content = """
print("Hello from my_script.py")
x = 1 + 2
if x == 3:
print("x is 3")
"""
# To simulate a file, we can use exec with a custom globals dict
# and then trace its execution within a temporary module scope.
import types
import sys
# Create a dummy module to hold our script content, so should_instrument_file can identify it
my_script_module = types.ModuleType('my_script')
my_script_module.__file__ = '<string>my_script.py'
sys.modules['my_script'] = my_script_module
# Execute the script content within the dummy module's namespace
with MyTracer():
exec(script_content, my_script_module.__dict__)
# Clean up the dummy module
del sys.modules['my_script']