How would you use sys.monitoring.set_local_events to trace only the two functions suspected of a floating-point rounding drift in a feature-flag service's 340-case regression pack?
answer
- Narrow the subscription, not the output
- One code object instead of the whole process
- Pass the code object, never the function
- Branch events plus DISABLE keep it flat
- set_local_events on suspect.__code__
basics
~20 sClaim a tool id, register callbacks, then call sys.monitoring.set_local_events on each suspect function's code instead of the global sys.monitoring.set_events. Only those code objects are instrumented, so the rest of the service and the passing cases run uninstrumented.
solid answer
~40 s`sys.monitoring.set_local_events(tool_id, code, event_set)` scopes a subscription to one code object, where `sys.monitoring.set_events` instruments everything the process runs. For a rounding drift the question is which side of a comparison each case takes, so subscribe `sys.monitoring.events.BRANCH_LEFT` and `sys.monitoring.events.BRANCH_RIGHT` — Python 3.14 split conditional-branch reporting into that pair — on `suspect.__code__`. It takes the code object, not the function: passing the function raises `TypeError: code must be a code object`. Return `sys.monitoring.DISABLE` once a branch edge is recorded, so 340 cases cost one callback per edge rather than 340. If the pack only ever reports one side, the drift is real. Only local events are accepted; `sys.monitoring.events.RAISE` raises `ValueError`. Tear down in `finally` with `NO_EVENTS` and `sys.monitoring.free_tool_id`.
code
python · 37 linesimport sys
TOOL_ID = sys.monitoring.DEBUGGER_ID
sides = set()
def rollout_fraction(bucket, weight):
if bucket * weight >= 0.5:
return 1.0
return 0.0
def on_left(code, offset, destination):
sides.add("left")
return sys.monitoring.DISABLE
def on_right(code, offset, destination):
sides.add("right")
return sys.monitoring.DISABLE
sys.monitoring.use_tool_id(TOOL_ID, "branch-trace")
sys.monitoring.register_callback(TOOL_ID, sys.monitoring.events.BRANCH_LEFT, on_left)
sys.monitoring.register_callback(TOOL_ID, sys.monitoring.events.BRANCH_RIGHT, on_right)
sys.monitoring.set_local_events(
TOOL_ID,
rollout_fraction.__code__,
sys.monitoring.events.BRANCH_LEFT | sys.monitoring.events.BRANCH_RIGHT,
)
for case in range(340):
rollout_fraction(case / 340, 0.5)
sys.monitoring.set_local_events(
TOOL_ID, rollout_fraction.__code__, sys.monitoring.events.NO_EVENTS
)
sys.monitoring.free_tool_id(TOOL_ID)
print("branch sides reached by the 340-case pack:", sorted(sides))go deeper
Recall that sys.monitoring can be pointed at one function rather than the whole program, and that the handle it wants is the function's code attribute rather than the function itself.
Explain how set_local_events differs from set_events, that it works with the global event set left at NO_EVENTS, and which events it accepts. Be able to pick BRANCH_LEFT and BRANCH_RIGHT for a branch-outcome question on 3.14.
Show the whole diagnostic: check get_tool before claiming a slot, scope to the suspect code objects, retire each location with DISABLE so a 340-case pack stays flat, tear down in finally, and know the C-function boundary.
Own how this becomes a repeatable capability — which tool slot in-house diagnostics reserve, whether scoped instrumentation is permitted against production processes, and when to invest in it versus reproducing the failure in a controlled harness.
The instinct when chasing a numerical bug is to switch on line tracing and grep the output. With `sys.monitoring` that is the expensive way round, because the API can instrument a *single code object* and leave the rest of the process untouched. ### The scenario A feature-flag service decides rollout percentages with floating-point arithmetic. A 340-case regression pack starts failing on a handful of cases: a bucket that should land just above a threshold lands just below it, and the flag flips off. The suspicion is a rounding drift in one comparison inside two small functions. You want to know which side of that comparison each case takes, without instrumenting the request path, the flag store, or the 300-odd cases that behave. ### `set_local_events` versus `set_events` `sys.monitoring.set_events(tool_id, event_set)` is the **global** switch: subscribe to `sys.monitoring.events.LINE` and every code object the process executes becomes instrumented. You then filter by filename inside the callback — which means you have already paid the dispatch you were trying to avoid, for every line of every unrelated function. `sys.monitoring.set_local_events(tool_id, code, event_set)` takes a **code object** and scopes the subscription to it. Nothing else in the process is instrumented. Two important properties follow: * It works independently of `set_events`. You can leave the global set at `sys.monitoring.events.NO_EVENTS` and still receive callbacks from the local subscription. * It takes the code object, not the function. `set_local_events(tool_id, my_func, ...)` raises `TypeError: code must be a code object`; you pass `my_func.__code__`. For a method on a class you do not own, `SomeClass.some_method.__code__` reaches it without touching the source or wrapping the callable — which is what makes this usable against third-party code in a running process. It also accepts only the **local** events: `PY_START`, `PY_RESUME`, `PY_RETURN`, `PY_YIELD`, `CALL`, `LINE`, `INSTRUCTION`, `JUMP`, `BRANCH_LEFT`, `BRANCH_RIGHT`, `STOP_ITERATION`. Passing `sys.monitoring.events.RAISE` raises `ValueError: invalid local event set`. ### Choosing the events For a branch-outcome question, `sys.monitoring.events.BRANCH_LEFT` and `sys.monitoring.events.BRANCH_RIGHT` are the right subscription. Python 3.14 added this pair, splitting the two outgoing edges of a conditional branch into distinct events; earlier versions had only the single `sys.monitoring.events.BRANCH`. Each callback receives `(code, instruction_offset, destination_offset)`, so you learn which comparison and which way it went. If you also need the operand values, add `sys.monitoring.events.LINE` and read them from the frame — but that is a bigger hammer, and for "which side did we take" the branch events alone answer the question. ### Keeping the cost flat across 340 cases Return `sys.monitoring.DISABLE` from the callback once a location has been recorded. The pack runs 340 cases through the same handful of instructions; without `DISABLE` you pay 340 callbacks per branch edge, with it you pay one. The recorded result is a *set* of branch edges reached, which is precisely the diagnostic: if the 340-case pack only ever reports one side of the comparison, the drift is real and the other side is unreachable under the pack's inputs. ```python sys.monitoring.set_local_events( tool_id, rollout_fraction.__code__, sys.monitoring.events.BRANCH_LEFT | sys.monitoring.events.BRANCH_RIGHT, ) ``` Call `sys.monitoring.restart_events()` between runs if you want a fresh set rather than a cumulative one. ### Operational discipline Three things separate a diagnostic you can run against a live service from one that leaves a mess: 1. **Check the slot first.** `sys.monitoring.get_tool(tool_id)` tells you whether a debugger or coverage recorder already holds it; `use_tool_id` raises `ValueError` if it does. There are only six slots. 2. **Tear down in `finally`.** Set the local events back to `sys.monitoring.events.NO_EVENTS` and call `sys.monitoring.free_tool_id(tool_id)`. Leaving a code object instrumented after the run means the service keeps paying for callbacks nobody is reading. 3. **Know the boundary.** If the arithmetic that drifts happens inside a C function, `sys.monitoring` gives you `C_RETURN` and `C_RAISE`, but those cannot be enabled independently — they must be set together with `sys.monitoring.events.CALL` — and they are global-only, so there is no per-code-object scoping for them. The overall shape is: narrow the subscription to the code you suspect, pick the smallest event that answers the question, retire each location once you have your answer, and put the interpreter back the way you found it. ### Reading the result The output of this trace is a set, not a log. Each entry says "this branch edge was reached at least once during the pack". That is a deliberately small answer, and it is usually the one you want: a comparison whose two edges should both be exercised by 340 varied inputs, but which only ever reports one, has told you the threshold is never crossed the way the test author assumed. From there the follow-up is arithmetic rather than instrumentation — reproduce the two inputs nearest the boundary and inspect the operands directly. If you do need the operand values, widen the subscription to `sys.monitoring.events.LINE` on the same code object and read the locals from the frame in the callback, and drop `sys.monitoring.DISABLE` for that run so you see every case rather than the first. That is a much more expensive trace, which is exactly why you scope it to one code object and run it only after the cheap branch-reachability pass has told you where to look. Narrow first, then deepen: the whole value of `set_local_events` is that it lets the expensive trace stay small enough to run against a realistic workload instead of a toy reproduction.
- How do you instrument a method on a class whose source you cannot change?Reach its code object directly: `SomeClass.some_method.__code__` and hand that to `sys.monitoring.set_local_events`. No wrapping, no monkey-patching, no source edit — the interpreter instruments the existing code object in place. The same trick works for a closure or a nested function through the enclosing function's `__code__.co_consts`, and it is what makes this usable against third-party code in a running process.
- Why not enable LINE globally and filter by filename inside the callback?Global `sys.monitoring.set_events` instruments every code object the process executes, so you pay a Python callback for every line of every unrelated function and then discard almost all of it. Filtering in the callback is exactly the cost you were trying to avoid. Scoping with set_local_events means the uninteresting code is never instrumented in the first place.
- What if the drifting arithmetic happens inside a C function?sys.monitoring reports `sys.monitoring.events.C_RETURN` and `sys.monitoring.events.C_RAISE`, but they cannot be enabled independently — the interpreter raises ValueError unless they are set together with `sys.monitoring.events.CALL` — and they are global-only, so there is no per-code-object scoping. You see that the C call happened and how it left, not what it did inside.
- How do you get a fresh reading rather than a cumulative one between runs?Call `sys.monitoring.restart_events()` between runs to clear every retired location, then re-run the pack. Be aware it is process-wide across all six tool slots, so it re-arms other tools' disabled locations too. If that matters, keep your own recorded set and reset that instead of restarting the interpreter's disable state.
saying these in an interview costs you the question
- Enables events globally then filters by filename in the callback
- Passes the function object instead of its __code__ attribute
- Expects set_local_events to accept RAISE or other global-only events
- Forgets free_tool_id, so the next diagnostic collides on the slot
- Leaves instrumentation armed in the service after the run
- Assumes local events require a matching global set_events call