Skip to content

Fix false positive unbound-name with NoReturn in except blocks#2486

Closed
Adist319 wants to merge 2 commits intofacebook:mainfrom
Adist319:fix/noreturn-try-except-unbound-name
Closed

Fix false positive unbound-name with NoReturn in except blocks#2486
Adist319 wants to merge 2 commits intofacebook:mainfrom
Adist319:fix/noreturn-try-except-unbound-name

Conversation

@Adist319
Copy link
Contributor

@Adist319 Adist319 commented Feb 21, 2026

Describe the Bug

When a variable is assigned in a try block and a NoReturn function is called in the except block, pyrefly incorrectly reports `node` may be uninitialized [unbound-name] when the try/except is nested inside an if statement. The simple (non-nested) case already works correctly.

from typing import NoReturn

def foo() -> NoReturn:
    raise ValueError('')

def main(resolve: bool) -> None:
    try:
        node = 1
    except Exception as exc:
        foo()
    if resolve:
        try:
            node = 2
        except Exception:
            foo()
    print(node)  # ERROR `node` may be uninitialized [unbound-name] - FALSE POSITIVE

Pyright handles this correctly. Since foo() returns NoReturn, the except paths always terminate, so node is guaranteed to be initialized at print(node).

Root Cause

In FlowStyle::merged(), when merging flow styles where any branch had MaybeInitialized (the deferred NoReturn/Never check style), the code immediately converted them to PossiblyUninitialized, discarding the termination keys needed for solve-time verification.

Fix

  • When merging two MaybeInitialized styles, combine their termination keys instead of discarding them
  • When merging MaybeInitialized with a fully initialized style, preserve the MaybeInitialized keys
  • Ensure Uninitialized/PossiblyUninitialized arms match before MaybeInitialized catch-all to prevent masking valid uninitialized paths

Test Plan

  • Added test_noreturn_try_except_simple — simple try/except with NoReturn (passes before and after)
  • Added test_noreturn_try_except_if_nested — exact reproduction from Erroneous unbound-name when using NoReturn #2406 (failed before, passes after)
  • All 106 flow_branching tests pass
  • All flow_looping, never, uninit, and scope tests pass

Fixes #2406.

When merging FlowStyles where branches had MaybeInitialized (deferred
NoReturn/Never checks), the merge incorrectly converted them to
PossiblyUninitialized, discarding the termination keys needed for
solve-time verification. This caused false positive `unbound-name`
errors when a variable was assigned in a try block with a NoReturn
function in the except handler, nested inside an if statement.

The fix preserves termination keys by combining them when merging
MaybeInitialized styles, allowing the solver to correctly verify
that all NoReturn paths terminate. Uninitialized/PossiblyUninitialized
arms are matched before MaybeInitialized catch-all to prevent masking
valid uninitialized paths.

Fixes facebook#2406
@meta-cla meta-cla bot added the cla signed label Feb 21, 2026
@github-actions
Copy link

Diff from mypy_primer, showing the effect of this PR on open source code:

stone (https://github.com/dropbox/stone)
- ERROR stone/cli.py:343:9-12: `api` may be uninitialized [unbound-name]
- ERROR stone/cli.py:360:16-19: `api` may be uninitialized [unbound-name]

mkosi (https://github.com/systemd/mkosi)
- ERROR mkosi/config.py:4955:36-42: `result` may be uninitialized [unbound-name]
- ERROR mkosi/config.py:4966:68-74: `result` may be uninitialized [unbound-name]
- ERROR mkosi/distribution/opensuse.py:143:46-52: `subdir` may be uninitialized [unbound-name]
- ERROR mkosi/distribution/opensuse.py:155:54-60: `subdir` may be uninitialized [unbound-name]
- ERROR mkosi/distribution/opensuse.py:164:46-52: `subdir` may be uninitialized [unbound-name]
- ERROR mkosi/distribution/opensuse.py:171:46-52: `subdir` may be uninitialized [unbound-name]

mongo-python-driver (https://github.com/mongodb/mongo-python-driver)
- ERROR bson/__init__.py:369:16-21: `value` may be uninitialized [unbound-name]
- ERROR bson/__init__.py:578:30-35: `value` may be uninitialized [unbound-name]

bokeh (https://github.com/bokeh/bokeh)
- ERROR src/bokeh/core/serialization.py:631:16-22: `buffer` may be uninitialized [unbound-name]

beartype (https://github.com/beartype/beartype)
- ERROR beartype/_check/convert/_reduce/_pep/redpep484612646.py:642:49-64: `typearg_to_hint` may be uninitialized [unbound-name]

mypy (https://github.com/python/mypy)
- ERROR mypy/main.py:1563:12-19: `targets` may be uninitialized [unbound-name]

prefect (https://github.com/PrefectHQ/prefect)
- ERROR src/prefect/cli/_cyclopts/cloud.py:280:8-31: `prompt_switch_workspace` may be uninitialized [unbound-name]
- ERROR src/prefect/cli/_cyclopts/cloud.py:436:21-31: `workspaces` may be uninitialized [unbound-name]
- ERROR src/prefect/cli/_cyclopts/work_queue.py:596:23-27: `runs` may be uninitialized [unbound-name]
- ERROR src/prefect/cli/_cyclopts/work_queue.py:608:8-12: `runs` may be uninitialized [unbound-name]
- ERROR src/prefect/cli/cloud/__init__.py:232:8-31: `prompt_switch_workspace` may be uninitialized [unbound-name]
- ERROR src/prefect/cli/cloud/__init__.py:365:21-31: `workspaces` may be uninitialized [unbound-name]
- ERROR src/prefect/cli/work_queue.py:529:23-27: `runs` may be uninitialized [unbound-name]
- ERROR src/prefect/cli/work_queue.py:541:8-12: `runs` may be uninitialized [unbound-name]
- ERROR src/prefect/events/cli/automations.py:434:19-23: `data` may be uninitialized [unbound-name]
- ERROR src/prefect/events/cli/automations.py:434:52-56: `data` may be uninitialized [unbound-name]
- ERROR src/prefect/events/cli/automations.py:435:28-32: `data` may be uninitialized [unbound-name]
- ERROR src/prefect/events/cli/automations.py:436:21-25: `data` may be uninitialized [unbound-name]
- ERROR src/prefect/events/cli/automations.py:437:28-32: `data` may be uninitialized [unbound-name]
- ERROR src/prefect/events/cli/automations.py:439:29-33: `data` may be uninitialized [unbound-name]
- ERROR src/prefect/events/cli/automations.py:533:52-56: `data` may be uninitialized [unbound-name]

pywin32 (https://github.com/mhammond/pywin32)
- ERROR com/win32com/client/makepy.py:439:16-20: `args` may be uninitialized [unbound-name]
- ERROR com/win32comext/adsi/demos/scp.py:485:8-17: `log_level` may be uninitialized [unbound-name]
- ERROR win32/scripts/setup_d.py:89:32-35: `key` may be uninitialized [unbound-name]

@meta-codesync
Copy link

meta-codesync bot commented Feb 23, 2026

@yangdanny97 has imported this pull request. If you are a Meta employee, you can view this in D94091327.

Copy link
Contributor

@stroxler stroxler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review automatically exported from Phabricator review in Meta.

@meta-codesync
Copy link

meta-codesync bot commented Feb 24, 2026

@yangdanny97 merged this pull request in f3fffb3.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Erroneous unbound-name when using NoReturn

3 participants