What three sys.monitoring calls must you make before an event callback fires?
answer
- Three independent calls, all required
- Identity first, then handler, then subscription
- Six numbered slots, zero through five
- use_tool_id, register_callback, set_events
basics
~10 sClaim a slot with sys.monitoring.use_tool_id, attach a function with sys.monitoring.register_callback, then turn the event on with sys.monitoring.set_events. Skip any one of the three and nothing is delivered; using an unclaimed tool id raises ValueError.
solid answer
~40 s`sys.monitoring` is CPython's instrumentation API, added in 3.12. Starting it is three calls. `sys.monitoring.use_tool_id(tool_id, name)` claims one of six slots numbered 0 to 5 and raises `ValueError` if another tool already holds it; the constants `sys.monitoring.DEBUGGER_ID`, `sys.monitoring.COVERAGE_ID`, `sys.monitoring.PROFILER_ID` and `sys.monitoring.OPTIMIZER_ID` name the conventional slots. `sys.monitoring.register_callback(tool_id, event, func)` attaches your function to one event and returns whatever callback was there before, so you can chain or restore it. `sys.monitoring.set_events(tool_id, event_set)` then switches events on; the argument is a bitwise OR of constants from the `sys.monitoring.events` namespace, and `sys.monitoring.events.NO_EVENTS` turns everything off. Tear down with `sys.monitoring.free_tool_id`. Enabling an event with no callback registered is not an error, just silence.
code
python · 23 linesimport sys
TOOL_ID = sys.monitoring.PROFILER_ID
def on_py_start(code, instruction_offset):
print("enter", code.co_name)
sys.monitoring.use_tool_id(TOOL_ID, "call-logger")
sys.monitoring.register_callback(
TOOL_ID, sys.monitoring.events.PY_START, on_py_start
)
sys.monitoring.set_events(TOOL_ID, sys.monitoring.events.PY_START)
def greet(name):
return "hello " + name
greet("python")
sys.monitoring.set_events(TOOL_ID, sys.monitoring.events.NO_EVENTS)
sys.monitoring.free_tool_id(TOOL_ID)go deeper
Be ready to name the three calls in order and say what each one does: claim the slot, attach the function, switch the event on. Knowing that all three are required is most of the answer.
Explain the mechanics: six numbered slots, the ValueError on a conflicting or unclaimed id, the event set as a bitwise OR of sys.monitoring.events constants, and register_callback returning the previous handler.
Show the operational discipline — check get_tool before claiming, wrap the run in try/finally, set NO_EVENTS and free_tool_id on the way out, and reason about coexisting with a debugger or coverage tool already in the process.
Own the policy question: whether your platform standardises which of the six slots each in-house tool takes, whether instrumentation may be left armed in production, and what the fallback is when all six slots are already claimed.
`sys.monitoring` is CPython's instrumentation API, introduced in 3.12 by PEP 669 and extended in 3.14. It exists so that debuggers, coverage recorders and profilers can be told *exactly* which runtime events they care about, and so the interpreter can compile instrumentation into only the code that is actually being watched. Getting events out of it is a fixed three-step handshake, and every step exists for a reason. ### Step one — claim a tool id ```python import sys sys.monitoring.use_tool_id(sys.monitoring.PROFILER_ID, "call-logger") ``` Instrumentation state in CPython is partitioned into **six independent slots**, numbered 0 through 5. Each slot is a *tool*: its own callbacks, its own subscribed event set, its own per-location disable state. Two tools can watch the same event in the same function without seeing each other's callbacks — which is the whole point, because a coverage recorder and a debugger routinely run in the same process. `sys.monitoring.use_tool_id(tool_id, name)` claims a slot and gives it a human-readable name. It raises `ValueError` if that slot is already in use (`tool 3 is already in use`) and also if the id is out of range (`invalid tool 6 (must be between 0 and 5)`). `sys.monitoring.get_tool(tool_id)` returns the registered name of whoever holds a slot, or `None` if it is free — call it before claiming if you want a graceful message rather than an exception. Four module-level constants name the conventional slots: `sys.monitoring.DEBUGGER_ID` (0), `sys.monitoring.COVERAGE_ID` (1), `sys.monitoring.PROFILER_ID` (2) and `sys.monitoring.OPTIMIZER_ID` (5). These are conventions, not enforcement — nothing stops a coverage tool from taking id 4 — but honouring them keeps unrelated tools out of each other's way. `sys.monitoring.free_tool_id(tool_id)` releases the slot. It is not automatic; a library that claims an id and never frees it holds one of six process-wide slots for the life of the interpreter. ### Step two — register a callback ```python sys.monitoring.register_callback( sys.monitoring.PROFILER_ID, sys.monitoring.events.PY_START, on_py_start ) ``` `register_callback(tool_id, event, func)` attaches one function to **one** event for one tool. It returns whatever callback was registered before — `None` on the first call — so a wrapper can chain to the previous handler or restore it afterwards. Passing `None` as the function unregisters. The callback signature is determined by the event, not by you. A `sys.monitoring.events.PY_START` handler receives `(code, instruction_offset)`; a `sys.monitoring.events.LINE` handler receives `(code, line_number)`; a `sys.monitoring.events.CALL` handler receives `(code, instruction_offset, callable, arg0)`, where `arg0` is the sentinel `sys.monitoring.MISSING` when the call has no first argument. Getting the arity wrong surfaces as a `TypeError` raised from inside the interpreter at the first event, which is a confusing place to debug, so check the signature for each event you subscribe. ### Step three — turn the events on ```python sys.monitoring.set_events( sys.monitoring.PROFILER_ID, sys.monitoring.events.PY_START ) ``` `set_events(tool_id, event_set)` is the switch. The argument is a **bit set**: the constants in the `sys.monitoring.events` namespace are integers you combine with `|`, and `sys.monitoring.events.NO_EVENTS` (zero) turns everything off for that tool. `sys.monitoring.get_events(tool_id)` reads the current set back. Note that `set_events` *replaces* the set rather than adding to it — to add an event, OR it into the value you read from `get_events`. Two failure modes matter here. Calling `set_events` for a slot you never claimed raises `ValueError: tool 4 is not in use` — this is the mistake that catches people who register a callback and expect it to work. And `sys.monitoring.events.C_RETURN` and `sys.monitoring.events.C_RAISE` cannot be enabled on their own; the interpreter raises `ValueError: cannot set C_RETURN or C_RAISE events independently`, because they are only tracked alongside `sys.monitoring.events.CALL`. ### What is *not* an error Enabling an event with no callback registered is silently fine — the event is tracked and nothing is delivered. Registering a callback without enabling the event is also fine, and equally silent. That symmetry is why "nothing happens" is the classic first bug with this API: the three calls are independent, and only all three together produce a callback. ### Teardown The disciplined shape is `set_events(tool_id, sys.monitoring.events.NO_EVENTS)` followed by `free_tool_id(tool_id)`, ideally in a `finally`. `sys.monitoring.clear_tool_id(tool_id)` is the shortcut that unregisters the callbacks and clears the events for a tool in one call while keeping the slot claimed. Leaving instrumentation armed after a diagnostic run means the process keeps paying for callbacks nobody reads. ### Why the handshake is shaped this way The three-step design is not ceremony. Because a tool declares its identity first, CPython can keep one instrumentation state per slot and decide, per code object and per instruction, which tools need a callback there — that is what lets a coverage recorder and a debugger coexist without either one seeing the other's events or paying for them. Because the subscription is a separate, explicit bit set rather than a single always-on hook, the interpreter knows the exact list of events to compile in, and can leave everything else running as ordinary bytecode. The practical consequence for you is a debugging checklist when no callbacks arrive. Confirm the slot is claimed (`sys.monitoring.get_tool` returns your name, not `None`). Confirm the event you registered is the same constant you enabled — subscribing to `sys.monitoring.events.CALL` and registering for `sys.monitoring.events.PY_START` is a silent mismatch. Confirm the enabled set is what you think it is by reading it back with `sys.monitoring.get_events`, remembering that `set_events` replaces rather than adds. And confirm the callback's arity matches the event, because a wrong signature surfaces as a `TypeError` raised from deep inside the interpreter rather than at the line you wrote.
- What happens when two libraries both try to claim sys.monitoring tool id 2?The second `sys.monitoring.use_tool_id` call raises `ValueError: tool 2 is already in use`. There are only six slots, 0 through 5, and ids outside that range raise as well. `sys.monitoring.get_tool(2)` returns the name of whoever holds the slot, or `None` if it is free, so a well-behaved tool checks first and reports a clear message. `sys.monitoring.free_tool_id` releases the slot; nothing releases it automatically.
- Does the order of register_callback and set_events matter?No. They are independent pieces of state and either order works. Enabling an event with no callback is a silent no-op, and registering a callback for an event that is not enabled simply never fires. What does matter is that the tool id is claimed first: `sys.monitoring.set_events` on an unclaimed slot raises `ValueError: tool 4 is not in use`.
- What does sys.monitoring.register_callback return?The callback previously registered for that tool and event, or `None` if there was none. That makes it easy to wrap an existing tool's handler and delegate to it, or to save and restore the original. Passing `None` as the function unregisters the callback without touching the tool's enabled event set.
Six radio channels the interpreter can broadcast on: you first reserve a channel, then plug in a receiver, then tell the station which programmes to transmit. Reserve and plug in but never subscribe, and the room stays quiet.
saying these in an interview costs you the question
- Thinks register_callback alone starts delivering events
- Believes any integer can be used as a tool id
- Calls set_events without claiming the tool id first
- Assumes an event with no callback raises an error
- Never calls free_tool_id, holding a slot for the process lifetime
- Confuses the tool id with the event bit mask