Skip to content

Make the PY_UNWIND monitoring event local #142186

Open

Description

@P403n1x87

Feature or enhancement

Proposal:

I would like to propose making the PY_UNWIND monitoring event available as a local event. The rationale for doing this is that some tools can be interested in trapping per-function exit events. Currently, this is only possible with the PY_RETURN event when a function returns with a value. However, if a function is exited with an exception, there is no way of capturing that event on just that function. Making the PY_UNWIND event would allow monitoring tools to catch all function exit events.

import random, sys

m, e = sys.monitoring, sys.monitoring.events

m.use_tool_id(0, "debugger")

def monitor(event):
    def _(f):
        m.register_callback(0, event, f)
    return _

@monitor(e.PY_START)
def _(*_, **__):
    print("entering", sys._getframe(1))

@monitor(e.PY_UNWIND)
def _(*_, **__):
    print("exiting ", sys._getframe(1), "with exception")

def foo():
    raise RuntimeError()

def bar():
    return foo()

def baz():
    return bar()

m.set_local_events(0, random.choice([foo, bar, baz]).__code__, e.PY_UNWIND | e.PY_START)

try:
    baz()
except Exception:
    pass

# entering <frame at 0x101602f80, file 'test_py_unwind_local.py', line 34, code baz>
# exiting  <frame at 0x101602f80, file 'test_py_unwind_local.py', line 35, code baz> with exception

A debugger is an example of a tool that can benefit from PY_UNWIND being a local event.

The proposed change will break the current assumption that local events always require bytecode instrumentation. Instead, the PY_UNWIND event will simply make a local tool check instead of a global one inside monitor_unwind. No changes to bytecode or any other parts are required.

This is an extension of the current implementation of PEP 669 whereby PY_UNWIND will be available as a local event. The behaviour of the already existing global event will not be touched. As such, the proposed change will be guaranteed to be backwards-compatible.

Has this already been discussed elsewhere?

I have already discussed this feature proposal on Discourse

Links to previous discussion of this feature:

https://discuss.python.org/t/pep-669-low-impact-monitoring-for-cpython/13018/82

Linked PRs

Activity

  1. markshannon commented on Dec 2, 2025

    @markshannon
    Member

    It should be possible to disable local events, and disabling is specific to the location passed to the callback.
    https://docs.python.org/3/library/sys.monitoring.html#local-events
    How would you implement that?

    Or do you want to make PY_UNWIND its own category, that can be turned on or off per code objects, but cannot be disabled?

    If we are breaking the distinction between "local" meaning specific to a location and can be disabled, and "local" meaning can be turned on or off per code object, then we might as well make all events "local" in the second sense.

  2. P403n1x87 commented on Dec 2, 2025

    @P403n1x87
    ContributorAuthor

    It should be possible to disable local events, and disabling is specific to the location passed to the callback. https://docs.python.org/3/library/sys.monitoring.html#local-events How would you implement that?

    My understanding is that DISABLE is intended for those local events that can trigger at multiple locations. It can be used to fine-tune the events that the user wants to trap (e.g. just a sub-set of all the lines within a code object, or certain return statements). PY_UNWIND is obviously an event that is not local in the sense of coming from a certain bytecode offset. However, one might argue that the event itself happens when the execution has reached a certain offset, so it (and in fact any event) is local in this other sense. But because these events do not instrument the bytecode, they cannot be DISABLEd like other local events.

    Or do you want to make PY_UNWIND its own category, that can be turned on or off per code objects, but cannot be disabled?

    Because as of now all local events require instrumentation, I would be more inclined to reflect the "new" meaning of local in the documentation. That is, for events that can trigger at pre-defined locations (think compile-time), returning DISABLE will do the current job, otherwise it is ignored. For example, PY_START is a local event with instrumentation at a guaranteed single location. Returning DISABLE from its handler would undo the instrumentation, which is effectively equivalent to turning the event off on the code object. So we already have a local event in its own category.

    If we are breaking the distinction between "local" meaning specific to a location and can be disabled, and "local" meaning can be turned on or off per code object, then we might as well make all events "local" in the second sense.

    Ideally I would like the idea of having all the events local on a per-code object basis. I haven't looked at the other events, but the PY_UNWIND one was easy enough to implement at potentially no performance cost. If it is equally convenient to turn the other currently non-local events into local ones in this new sense then I think it would be great.

  3. P403n1x87 commented on Dec 5, 2025

    @P403n1x87
    ContributorAuthor

    @markshannon for the sake of making progress here, would it be acceptable to merge the proposed change that makes PY_UNWIND "local" (and ignores DISABLE), and follow that up with potentially a more comprehensive change that makes all other events local in this new sense, and restrict the relevance of DISABLE to only some of the events? As I mentioned in my previous comment, the PY_START event has a similar behaviour. The fact that it can be disabled but not turned off is just a mere consequence of the implementation (which I think is hacky anyway, as shown by the "funny" coding required to handle that case in #142136), since the instrumentation for that event takes place in just a single place, regardless of the bytecode content.

  4. P403n1x87 commented on Jan 13, 2026

    @P403n1x87
    ContributorAuthor

    Gentle ping. I would really like for this change to make it into 3.15 if possible 🙏

  5. P403n1x87 commented on Mar 3, 2026

    @P403n1x87
    ContributorAuthor

    @markshannon apologies for the direct ping but I would love to resume discussing this as it is blocking our plans to move to the low-impact monitoring API for our instrumentation needs 🙏

  6. markshannon commented on Mar 5, 2026

    @markshannon
    Member

    Sorry for taking so long to get back to this.

    Rather than making PY_UNWIND local, what I think would be a better solution and meet your needs, is this:

    • Make "global" events per code-objects, so that they can all be set per code object
    • Allowing DISABLEing of "global" events, which would disable them per code object, not per location.

    This should only take a little bit of extra space per code object and have no impact on performance (at least not for the uninstrumented case). We might need to change the name "global" and the docs will definitely need updating.

    How does that sound?

  7. markshannon commented on Mar 5, 2026

    @markshannon
    Member

    OOI, why do you need per code object unwind events. Would filtering a global event be too expensive?

  8. P403n1x87 commented on Mar 5, 2026

    @P403n1x87
    ContributorAuthor

    Sorry for taking so long to get back to this.

    No problem, thanks for taking another look at this 🙂 .

    How does that sound?

    I totally agree on all points. However, just a note that our priority is just with PY_UNWIND as it's the only non-local event that would allow us to migrate to PEP-669. This is the least effort that would take us to where we'd like to be. I'll try to get a sense of the work required if the implementation should fall on our side.

    why do you need per code object unwind events

    At Datadog we provide dynamic instrumentation capabilities on live systems whereby we inject line probes and wrap functions to capture arguments/locals/return values/exceptions. For applications that throw exceptions for control flow, the cost of running a trap for every code object only to filter out just a few (where a probe is actually set) would be too expensive, so we would really like to target just the code objects we care about.

  9. P403n1x87 commented on Mar 23, 2026

    @P403n1x87
    ContributorAuthor

    @markshannon I have opened #146182 to make all the remaining events local per-code object.

  10. added a commit that references this issue on Apr 18, 2026
  11. added a commit that references this issue on Apr 22, 2026
  12. da-woods commented on Apr 23, 2026

    @da-woods
    Contributor

    Was there a reason to swap PY_MONITORING_EVENT_PY_UNWIND and PY_MONITORING_EVENT_RAISE? From looking at the changes I can't see anything obvious.

    I ask because I've got an overly fragile doctest that turns out to depend on the numeric order of the events. I can totally work around it (it's only a fragile doctest and no actual important behaviour). But it'd be nice not to have to change it unless there was a good reason to :-)

  13. P403n1x87 commented on Apr 24, 2026

    @P403n1x87
    ContributorAuthor

    Was there a reason to swap PY_MONITORING_EVENT_PY_UNWIND and PY_MONITORING_EVENT_RAISE? From looking at the changes I can't see anything obvious.

    No that was a relic of an initial implementation whereby only PY_UNWIND was made local per code object. So effectively that turned out to be noise in the git history of that source. I'm happy to make a change to revert that swap. However in your Cython issue you mention the fact that these IDs are opaque, so I wonder if it wouldn't make sense for Cython to try and avoid relying on the literal values anyway, if possible?

  14. scoder commented on Apr 24, 2026

    @scoder
    Contributor

    I think we should do both, revert the ID change in CPython to prevent a user visible impact without need, and make the test in Cython more robust by relying on the names rather than the IDs.

  15. scoder commented on Apr 24, 2026

    @scoder
    Contributor
  16. added a commit that references this issue on Apr 25, 2026
  17. scoder commented on Apr 25, 2026

    @scoder
    Contributor

    I've merged #148955 now to make sure this bit of churn gets enough time to settle before beta-1.

  18. P403n1x87 commented on Apr 25, 2026

    @P403n1x87
    ContributorAuthor

    Thank you for taking care of this 🙇

  19. added 2 commits that reference this issue on Apr 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)type-featureA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions