[mypyc] Fix AttributeError in async try/finally with mixed return paths by Chainfire · Pull Request #19361 · python/mypy

@Chainfire

Async functions with try/finally blocks were raising AttributeError when:
- Some paths in the try block return while others don't
- The non-return path is executed at runtime
- No further await calls are needed

This occurred because mypyc's IR requires all control flow paths to assign
to spill targets (temporary variables stored as generator attributes). The
non-return path assigns NULL to maintain this invariant, but reading NULL
attributes raises AttributeError in Python.

Created a new IR operation `GetAttrNullable` that can read NULL attributes
without raising AttributeError. This operation is used specifically in
try/finally resolution when reading spill targets.

- Added `GetAttrNullable` class to mypyc/ir/ops.py with error_kind=ERR_NEVER
- Added `read_nullable_attr` method to IRBuilder for creating these operations
- Modified `try_finally_resolve_control` in statement.py to use GetAttrNullable
  only for spill targets (attributes starting with '__mypyc_temp__')
- Implemented C code generation in emitfunc.py that reads attributes without
  NULL checks and only increments reference count if not NULL
- Added visitor implementations to all required files:
  - ir/pprint.py (pretty printing)
  - analysis/dataflow.py (dataflow analysis)
  - analysis/ircheck.py (IR validation)
  - analysis/selfleaks.py (self leak analysis)
  - transform/ir_transform.py (IR transformation)

1. **Separate operation vs flag**: Created a new operation instead of adding
   a flag to GetAttr for better performance - avoids runtime flag checks on
   every attribute access.

2. **Targeted fix**: Only applied to spill targets in try/finally resolution,
   not a general replacement for GetAttr. This minimizes risk and maintains
   existing behavior for all other attribute access.

3. **No initialization changes**: Initially tried initializing spill targets
   to Py_None instead of NULL, but this would incorrectly make try/finally
   blocks return None instead of falling through to subsequent code.

Added two test cases to mypyc/test-data/run-async.test:

1. **testAsyncTryFinallyMixedReturn**: Tests the basic issue with async
   try/finally blocks containing mixed return/non-return paths.

2. **testAsyncWithMixedReturn**: Tests async with statements (which use
   try/finally under the hood) to ensure the fix works for this common
   pattern as well.

Both tests verify that the AttributeError no longer occurs when taking
the non-return path through the try block.

See mypyc/mypyc#1115

@Chainfire mentioned this pull request

Jun 30, 2025

@Chainfire

JukkaL

@Chainfire

esarp pushed a commit that referenced this pull request

Jul 10, 2025
…hs (#19361)

Async functions with try/finally blocks were raising AttributeError
when:

* Some paths in the try block return while others don't
* The non-return path is executed at runtime
* No further await calls are needed

This occurred because mypyc's IR requires all control flow paths to
assign
to spill targets. The non-return path assigns NULL to maintain this
invariant, but reading NULL attributes raises AttributeError in Python.

Modified the GetAttr IR operation to support reading NULL attributes
without raising AttributeError through a new allow_null parameter. This
parameter is used specifically in try/finally resolution when reading
spill targets.

* Added allow_null: bool = False parameter to GetAttr.init in
mypyc/ir/ops.py
* When allow_null=True, sets error_kind=ERR_NEVER to prevent
AttributeError
* Modified read_nullable_attr in IRBuilder to create GetAttr with
allow_null=True
* Modified try_finally_resolve_control in statement.py to use
read_nullable_attr
  only for spill targets (attributes starting with 'mypyc_temp')
* Updated C code generation in emitfunc.py:
* visit_get_attr checks for allow_null and delegates to
get_attr_with_allow_null
* get_attr_with_allow_null reads attributes without NULL checks and only
    increments reference count if not NULL

Design decisions:

* Targeted fix: Only applied to spill targets in try/finally resolution,
not a general replacement for GetAttr. This minimizes risk and maintains
  existing behavior for all other attribute access.

* No initialization changes: Initially tried initializing spill targets
to Py_None instead of NULL, but this would incorrectly make try/finally
  blocks return None instead of falling through to subsequent code.

Added two test cases to mypyc/test-data/run-async.test:

* testAsyncTryFinallyMixedReturn: Tests the basic issue with async
  try/finally blocks containing mixed return/non-return paths.

* testAsyncWithMixedReturn: Tests async with statements (which use
  try/finally under the hood) to ensure the fix works for this common
  pattern as well.

Both tests verify that the AttributeError no longer occurs when taking
the non-return path through the try block.

See mypyc/mypyc#1115