Skip to content

sys.monitoring: wrong branch events for async comprehensions and for a second async for loop #162027

Description

@0z5a

Bug report

Bug description:

sys.monitoring branch events for async loops are wrong in two cases:

import asyncio, sys
M = sys.monitoring; E = M.events; M.use_tool_id(3, "t"); ev = []
M.register_callback(3, E.BRANCH_LEFT, lambda c, s, d: ev.append(("L", s, d)))
M.register_callback(3, E.BRANCH_RIGHT, lambda c, s, d: ev.append(("R", s, d)))
async def agen(): yield 1; yield 2
async def comp(): return [x async for x in agen()]
async def two():
    async for x in agen(): pass
    async for x in agen(): pass
for f in (comp, two):
    M.set_local_events(3, f.__code__, E.BRANCH_LEFT | E.BRANCH_RIGHT); ev.clear()
    asyncio.run(f()); print(f.__name__, ev, list(f.__code__.co_branches()))

Output with 3.14.3:

comp [('R', 48, 64)] [(48, 52, 64)]
two [('L', 40, 44), ('L', 40, 44), ('R', 40, 56), ('L', 92, 96), ('L', 92, 96), ('R', 90, 104)] [(40, 44, 56), (90, 94, 104)]
  1. The async comprehension never emits BRANCH_LEFT, although the loop body runs twice. The first async for loop in two emits it once per item, as expected.
  2. In the second async for loop of two, BRANCH_RIGHT has source offset 90 and BRANCH_LEFT has source offset 92. co_branches() says (90, 94, 104), so the BRANCH_LEFT events match neither its source nor its left target. Offset 90 is the CLEANUP_THROW of that loop and 92 is its END_SEND.

Expected: BRANCH_LEFT once per item and BRANCH_RIGHT once per loop. For each loop, its BRANCH_LEFT and BRANCH_RIGHT events share one source offset, and co_branches() lists that source with the same targets as the events.

With only one of the two functions in the loop, a debug build of main (cc98745) aborts on each. For comp, in co_branches():

Assertion failed: (_Py_GetBaseCodeUnit(bi->bi_code, src_offset+1).op.code == NOT_TAKEN), function branchesiter_next, file instrumentation.c, line 3183.

For two, while the monitored function runs:

Assertion failed: ((next_instr-oparg)->op.code == END_SEND || (next_instr-oparg)->op.code >= MIN_INSTRUMENTED_OPCODE), function _PyEval_EvalFrameDefault, file generated_cases.c.h, line 7553.

Tools that measure branch coverage with these events see the body of an async comprehension as never entered, and cannot pair the two directions of the second loop.

gh-128534 (#130569) added NOT_TAKEN after the END_SEND in codegen_async_for(); codegen_async_comprehension_generator() does not have it. gh-123076 is the older report about async for; gh-161928 is a different problem in the same events (exception from __anext__).

I have a small fix for both cases (Python/codegen.c, Python/assemble.c) with tests in test_monitoring and can open a PR.

CPython versions tested on:

3.14, CPython main branch

Operating systems tested on:

macOS

Output from running 'python -VV' on the command line:

Python 3.14.3 (main, Mar 25 2026, 03:28:18) [Clang 22.1.1 ]
Python 3.16.0a2+dev (heads/main:cc987451e8, Nov 14 2026, 23:57:44) [Clang 21.0.0 (clang-2100.1.1.101)]

Linked PRs

Activity

  1. itxtarun2727 commented on Nov 15, 2026

    @itxtarun2727

    i am reproducing this in my local environment , will ship the PR soon

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions