Skip to content

sys.monitoring: async for emits BRANCH_RIGHT when __anext__ raises an exception other than StopAsyncIteration #161928

Open

Description

@KRRT7

Bug report

Bug description:

When an async for loop ends because __anext__ raises an exception other than StopAsyncIteration, sys.monitoring still emits the loop-exit BRANCH_RIGHT event, to the statement after the loop. That statement never runs: the exception propagates. A synchronous for loop whose iterator raises emits no branch event, as expected.

This makes coverage.py report the statement after such a loop as executed with branch coverage (coveragepy/coveragepy#2303).

import asyncio, dis, sys

async def numbers(fail):
    yield 1
    if fail:
        raise ValueError("done")

async def consume(fail):
    async for n in numbers(fail):
        pass
    return "after loop"

M = sys.monitoring
E = M.events
ops = {i.offset: (i.opname, i.positions.lineno) for i in dis.get_instructions(consume)}
events = []

def branch(kind):
    def cb(code, src, dst):
        events.append(f"{kind} {ops.get(src)} -> {ops.get(dst)}")
    return cb

M.use_tool_id(3, "probe")
M.register_callback(3, E.BRANCH_LEFT, branch("BRANCH_LEFT "))
M.register_callback(3, E.BRANCH_RIGHT, branch("BRANCH_RIGHT"))
M.register_callback(3, E.LINE, lambda code, line: events.append(f"LINE {line}"))
M.set_local_events(3, consume.__code__, E.BRANCH_LEFT | E.BRANCH_RIGHT | E.LINE)

for fail in (False, True):
    events.clear()
    try:
        asyncio.run(consume(fail))
    except ValueError:
        pass
    print(f"fail={fail}:", *events, sep="\n    ")

Output on 3.14.7 and 3.15.1:

fail=False:
    LINE 9
    BRANCH_LEFT  ('NOT_TAKEN', 9) -> ('STORE_FAST', 9)
    LINE 10
    LINE 9
    BRANCH_RIGHT ('END_SEND', 9) -> ('LOAD_CONST', 11)
    LINE 11
fail=True:
    LINE 9
    BRANCH_LEFT  ('NOT_TAKEN', 9) -> ('STORE_FAST', 9)
    LINE 10
    LINE 9
    BRANCH_RIGHT ('END_SEND', 9) -> ('LOAD_CONST', 11)

With fail=True the BRANCH_RIGHT to line 11 is reported, but line 11 never runs.

In Python/bytecodes.c, INSTRUMENTED_END_ASYNC_FOR is _MONITOR_END_ASYNC_FOR + _END_ASYNC_FOR, so the branch event is emitted before _END_ASYNC_FOR checks for StopAsyncIteration and re-raises anything else. That order goes back to when these events were added in gh-130569 (gh-128534). Emitting the event only when the exception is a StopAsyncIteration should fix it, though I haven't tried that.

CPython versions tested on:

3.14, 3.15

Operating systems tested on:

macOS

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    interpreter-core(Objects, Python, Grammar, and Parser dirs)
    on Nov 13, 2026
  2. hu-matin commented on Nov 13, 2026

    @hu-matin

    Hi, I'd like to work on this issue and investigate a fix.

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-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions