Building or understanding developer tools requires knowing how Python reports calls, lines and returns.
Python lets tools register callbacks that the interpreter calls as code runs.
sys.settrace(func) installs a trace function for the current thread. It is called with (frame, event, arg) for 'call' events; whatever it returns becomes the local tracer for that frame, receiving 'line', 'return' and 'exception' events. pdb and older coverage tools are built on it. sys.setprofile is similar but only reports calls and returns, including C functions. threading.settrace installs a tracer for new threads. Both slow code down heavily because every line goes through a Python callback.
sys.monitoring (3.12, PEP 669) is the modern replacement. A tool claims an ID (DEBUGGER_ID, COVERAGE_ID, PROFILER_ID, OPTIMIZER_ID), registers callbacks per event (PY_START, PY_RETURN, LINE, BRANCH, CALL, RAISE...) and enables only the events it needs, globally or per code object. A callback can return sys.monitoring.DISABLE to switch off an event at a specific location, so a coverage tool pays for each line only once. Coverage.py and debuggers on 3.12+ use it.
Frames expose f_code.co_name, f_lineno and f_locals, which is how debuggers show variables. Always remove hooks when done and free the tool ID.
Example
import sys
mon = sys.monitoring
TOOL = mon.PROFILER_ID
mon.use_tool_id(TOOL, "call-counter")
calls = {}
def on_start(code, offset):
calls[code.co_name] = calls.get(code.co_name, 0) + 1
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
mon.register_callback(TOOL, mon.events.PY_START, on_start)
mon.set_events(TOOL, mon.events.PY_START)
fib(10)
mon.set_events(TOOL, 0)
mon.free_tool_id(TOOL)
print(calls["fib"]) # 177
lines = []
def tracer(frame, event, arg):
if event == "line":
lines.append(frame.f_lineno)
return tracer
sys.settrace(tracer); fib(1); sys.settrace(None)
print(len(lines)) # 1Remember
sys.settracecalls a Python function on call, line, return and exception eventssys.monitoring(3.12+) enables only chosen events per tool, with far lower overhead- Return
DISABLEfrom a monitoring callback to stop events at that location
Watch out for
- Leaving a trace function installed; it slows every line of the program.