[mypyc] Fix exception swallowing in async try/finally blocks with await by Chainfire · Pull Request #19353 · python/mypy

@Chainfire Chainfire changed the title [mypyc] Test cases and compilation error for mypyc-try-finally-await [mypyc] Fix exception swallowing in async try/finally blocks with await

Jun 28, 2025
Any await that causes a context switch inside a finally block
swallows any exception (re-)raised in the preceding try or except
blocks. As the exception 'never happened', this also changes
control flow.

This commit adds several tests (which fail) for this bug, and
triggers a compiler error if this pattern is detected in the code.
`# type: ignore[mypyc-try-finally-await, unused-ignore]` can be
used on the try line to bypass the error.

This also newly causes the testAsyncReturn test to fail, as it
should.

See mypyc/mypyc#1114
When a try/finally block in an async function contains an await statement
in the finally block, exceptions raised in the try block are silently
swallowed if a context switch occurs. This happens because mypyc stores
exception information in registers that don't survive across await points.

The Problem:
- mypyc's transform_try_finally_stmt uses error_catch_op to save exceptions
  to a register, then reraise_exception_op to restore from that register
- When await causes a context switch, register values are lost
- The exception information is gone, causing silent exception swallowing

The Solution:
- Add new transform_try_finally_stmt_async for async-aware exception handling
- Use sys.exc_info() to preserve exceptions across context switches instead
  of registers
- Check error indicator first to handle new exceptions raised in finally
- Route to async version when finally block contains await expressions

Implementation Details:
- transform_try_finally_stmt_async uses get_exc_info_op/restore_exc_info_op
  which work with sys.exc_info() that survives context switches
- Proper exception priority: new exceptions in finally replace originals
- Added has_await_in_block helper to detect await expressions

Test Coverage:
Added comprehensive async exception handling tests:
- testAsyncTryExceptFinallyAwait: 8 test cases covering various scenarios
  - Simple try/finally with exception and await
  - Exception caught but not re-raised
  - Exception caught and re-raised
  - Different exception raised in except
  - Try/except inside finally block
  - Try/finally inside finally block
  - Control case without await
  - Normal flow without exceptions
- testAsyncContextManagerExceptionHandling: Verifies async with still works
  - Basic exception propagation
  - Exception in __aexit__ replacing original

  See mypyc/mypyc#1114

@pre-commit-ci @Chainfire

@Chainfire

@Chainfire

esarp pushed a commit that referenced this pull request

Jul 10, 2025
…it (#19353)

When a try/finally block in an async function contains an await statement
in the finally block, exceptions raised in the try block are silently
swallowed if a context switch occurs. This happens because mypyc stores
exception information in registers that don't survive across await points.

The Problem:

- mypyc's transform_try_finally_stmt uses error_catch_op to save exceptions
- to a register, then reraise_exception_op to restore from that register
- When await causes a context switch, register values are lost
- The exception information is gone, causing silent exception swallowing

The Solution:

- Add new transform_try_finally_stmt_async for async-aware exception handling
- Use sys.exc_info() to preserve exceptions across context switches instead
- of registers
- Check error indicator first to handle new exceptions raised in finally
- Route to async version when finally block contains await expressions

Implementation Details:

- transform_try_finally_stmt_async uses get_exc_info_op/restore_exc_info_op
- which work with sys.exc_info() that survives context switches
- Proper exception priority: new exceptions in finally replace originals
- Added has_await_in_block helper to detect await expressions

Test Coverage:

Added comprehensive async exception handling tests:

- testAsyncTryExceptFinallyAwait: 8 test cases covering various scenarios
    - Simple try/finally with exception and await
    - Exception caught but not re-raised
    - Exception caught and re-raised
    - Different exception raised in except
    - Try/except inside finally block
    - Try/finally inside finally block
    - Control case without await
    - Normal flow without exceptions
- testAsyncContextManagerExceptionHandling: Verifies async with still works
    - Basic exception propagation
    - Exception in **aexit** replacing original

See mypyc/mypyc#1114.

Jdubz added a commit to Jdubz/blinky_time that referenced this pull request

Apr 2, 2026
scoring.py type safety (review feedback):
  Added _DetectionDict and _MusicStateDict TypedDicts to replace
  dict[str, Any] intermediates. Removes all type: ignore comments
  from the scoring pipeline. Mypy now fully validates field access
  on detection and music state dicts.

routes_devices.py inline import (review feedback):
  Moved `from ..firmware import upload_firmware` to top-level import.
  No circular dependency exists.

MCP disconnect description (review feedback):
  Updated tool description to clarify it stops streaming only, does
  not release the transport.

mypy pin (review feedback):
  Added upstream issue URL to the <1.20 pin comment
  (python/mypy#19353).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>