Skip to content

Extend flow system to consider simple predicates to avoid FP on initialized variables (#3253)#3253

Open
migeed-z wants to merge 1 commit intofacebook:mainfrom
migeed-z:export-D101709050
Open

Extend flow system to consider simple predicates to avoid FP on initialized variables (#3253)#3253
migeed-z wants to merge 1 commit intofacebook:mainfrom
migeed-z:export-D101709050

Conversation

@migeed-z
Copy link
Copy Markdown
Contributor

@migeed-z migeed-z commented Apr 28, 2026

Summary:

Pyrefly reports a false positive unbound-name error when a variable is
defined inside if a: and used inside a subsequent if a: with the same
condition. Since a is never modified, the variable is guaranteed to be
initialized whenever a is truthy.

This diff adds tracking to the flow system so we can keep track of this data. To start, we only handle predicates of the form if a: ... However, I added some comments on where I think we can extend the infrastructure to more general predicates.

Looking at the FP, it appears that adding support for even the basic predicates, decreases the FP by ~300.

Differential Revision: D101709050

@meta-cla meta-cla Bot added the cla signed label Apr 28, 2026
@meta-codesync
Copy link
Copy Markdown
Contributor

meta-codesync Bot commented Apr 28, 2026

@migeed-z has exported this pull request. If you are a Meta employee, you can view the originating Diff in D101709050.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@meta-codesync meta-codesync Bot changed the title Extend flow system to consider simple predicates to avoid FP on initialized variables Extend flow system to consider simple predicates to avoid FP on initialized variables (#3253) Apr 28, 2026
migeed-z added a commit to migeed-z/pyrefly that referenced this pull request Apr 28, 2026
…alized variables (facebook#3253)

Summary:

Pyrefly reports a false positive `unbound-name` error when a variable is
defined inside `if a:` and used inside a subsequent `if a:` with the same
condition. Since `a` is never modified, the variable is guaranteed to be
initialized whenever `a` is truthy.

This diff adds tracking to the flow system so we can keep track of this data. To start, we only handle predicates of the form `if a: ..`. However, I added some comments on where I think we can extend the infrastructure to more general predicates.

Looking at the FP, it appears that adding support for even the basic predicates, decreases the FP by ~300.

Differential Revision: D101709050
@migeed-z migeed-z force-pushed the export-D101709050 branch from 3c91f6d to 1ca6cbb Compare April 28, 2026 02:29
@github-actions github-actions Bot added size/xl and removed size/xl labels Apr 28, 2026
@github-actions

This comment has been minimized.

migeed-z added a commit to migeed-z/pyrefly that referenced this pull request Apr 28, 2026
…alized variables (facebook#3253)

Summary:

Pyrefly reports a false positive `unbound-name` error when a variable is
defined inside `if a:` and used inside a subsequent `if a:` with the same
condition. Since `a` is never modified, the variable is guaranteed to be
initialized whenever `a` is truthy.

This diff adds tracking to the flow system so we can keep track of this data. To start, we only handle predicates of the form `if a: ..`. However, I added some comments on where I think we can extend the infrastructure to more general predicates.

Looking at the FP, it appears that adding support for even the basic predicates, decreases the FP by ~300.

Differential Revision: D101709050
@migeed-z migeed-z force-pushed the export-D101709050 branch from 1ca6cbb to 260fc86 Compare April 28, 2026 03:04
@github-actions github-actions Bot added size/xl and removed size/xl labels Apr 28, 2026
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

migeed-z added a commit to migeed-z/pyrefly that referenced this pull request Apr 28, 2026
…alized variables (facebook#3253)

Summary:

Pyrefly reports a false positive `unbound-name` error when a variable is
defined inside `if a:` and used inside a subsequent `if a:` with the same
condition. Since `a` is never modified, the variable is guaranteed to be
initialized whenever `a` is truthy.

This diff adds tracking to the flow system so we can keep track of this data. To start, we only handle predicates of the form `if a: ..`. However, I added some comments on where I think we can extend the infrastructure to more general predicates.

Looking at the FP, it appears that adding support for even the basic predicates, decreases the FP by ~300.

Differential Revision: D101709050
@migeed-z migeed-z force-pushed the export-D101709050 branch from 260fc86 to 18191ca Compare April 28, 2026 03:50
@github-actions github-actions Bot added size/xl and removed size/xl labels Apr 28, 2026
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@migeed-z migeed-z force-pushed the export-D101709050 branch from 18191ca to d9a1b17 Compare April 28, 2026 17:20
migeed-z added a commit to migeed-z/pyrefly that referenced this pull request Apr 28, 2026
…alized variables (facebook#3253)

Summary:

Pyrefly reports a false positive `unbound-name` error when a variable is
defined inside `if a:` and used inside a subsequent `if a:` with the same
condition. Since `a` is never modified, the variable is guaranteed to be
initialized whenever `a` is truthy.

This diff adds tracking to the flow system so we can keep track of this data. To start, we only handle predicates of the form `if a: ..`. However, I added some comments on where I think we can extend the infrastructure to more general predicates.

Looking at the FP, it appears that adding support for even the basic predicates, decreases the FP by ~300.

Differential Revision: D101709050
@github-actions github-actions Bot removed the size/xl label Apr 28, 2026
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

migeed-z added a commit to migeed-z/pyrefly that referenced this pull request Apr 28, 2026
…alized variables (facebook#3253)

Summary:

Pyrefly reports a false positive `unbound-name` error when a variable is
defined inside `if a:` and used inside a subsequent `if a:` with the same
condition. Since `a` is never modified, the variable is guaranteed to be
initialized whenever `a` is truthy.

This diff adds tracking to the flow system so we can keep track of this data. To start, we only handle predicates of the form `if a: ..`. However, I added some comments on where I think we can extend the infrastructure to more general predicates.

Looking at the FP, it appears that adding support for even the basic predicates, decreases the FP by ~300.

Differential Revision: D101709050
@migeed-z migeed-z force-pushed the export-D101709050 branch from d9a1b17 to 6371d33 Compare April 28, 2026 18:44
@github-actions github-actions Bot added size/xl and removed size/xl labels Apr 28, 2026
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

…alized variables (facebook#3253)

Summary:

Pyrefly reports a false positive `unbound-name` error when a variable is
defined inside `if a:` and used inside a subsequent `if a:` with the same
condition. Since `a` is never modified, the variable is guaranteed to be
initialized whenever `a` is truthy.

This diff adds tracking to the flow system so we can keep track of this data. To start, we only handle predicates of the form `if a: ..`. However, I added some comments on where I think we can extend the infrastructure to more general predicates.

Looking at the FP, it appears that adding support for even the basic predicates, decreases the FP by ~300.

Differential Revision: D101709050
@github-actions
Copy link
Copy Markdown

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

openlibrary (https://github.com/internetarchive/openlibrary)
- ERROR openlibrary/plugins/upstream/borrow.py:180:28-39: `ia_itemname` may be uninitialized [unbound-name]
- ERROR openlibrary/plugins/upstream/borrow.py:180:47-54: `s3_keys` may be uninitialized [unbound-name]

cloud-init (https://github.com/canonical/cloud-init)
- ERROR tests/unittests/test_util.py:614:27-35: `confd_fn` may be uninitialized [unbound-name]

egglog-python (https://github.com/egraphs-good/egglog-python)
- ERROR python/egglog/egraph.py:1237:21-28: `egraphs` may be uninitialized [unbound-name]
- ERROR python/egglog/egraph.py:1244:17-24: `egraphs` may be uninitialized [unbound-name]
- ERROR python/egglog/egraph.py:1250:42-49: `egraphs` may be uninitialized [unbound-name]

rotki (https://github.com/rotki/rotki)
- ERROR rotkehlchen/db/drivers/gevent.py:211:117-127: `identifier` may be uninitialized [unbound-name]
- ERROR rotkehlchen/db/drivers/gevent.py:482:98-108: `identifier` may be uninitialized [unbound-name]
- ERROR rotkehlchen/tests/api/test_database.py:55:29-45: `backup2_contents` may be uninitialized [unbound-name]
- ERROR rotkehlchen/tests/api/test_database.py:57:29-45: `backup1_contents` may be uninitialized [unbound-name]

pytest (https://github.com/pytest-dev/pytest)
- ERROR src/_pytest/terminal.py:1266:17-26: `fullwidth` may be uninitialized [unbound-name]
- ERROR src/_pytest/terminal.py:1274:13-22: `fullwidth` may be uninitialized [unbound-name]
- ERROR src/_pytest/terminal.py:1281:13-22: `fullwidth` may be uninitialized [unbound-name]
- ERROR src/_pytest/terminal.py:1285:48-57: `fullwidth` may be uninitialized [unbound-name]

cryptography (https://github.com/pyca/cryptography)
- ERROR src/cryptography/hazmat/primitives/serialization/ssh.py:1091:25-34: `cert_body` may be uninitialized [unbound-name]
- ERROR src/cryptography/hazmat/primitives/serialization/ssh.py:1105:13-18: `nonce` may be uninitialized [unbound-name]

scikit-learn (https://github.com/scikit-learn/scikit-learn)
- ERROR sklearn/cluster/_agglomerative.py:388:13-22: `distances` may be uninitialized [unbound-name]
- ERROR sklearn/cluster/_agglomerative.py:422:35-44: `distances` may be uninitialized [unbound-name]
- ERROR sklearn/ensemble/_gb.py:922:28-40: `y_oob_masked` may be uninitialized [unbound-name]
- ERROR sklearn/ensemble/_gb.py:924:35-59: `sample_weight_oob_masked` may be uninitialized [unbound-name]
- ERROR sklearn/gaussian_process/_gpr.py:654:36-59: `log_likelihood_gradient` may be uninitialized [unbound-name]
- ERROR sklearn/linear_model/_coordinate_descent.py:182:34-40: `X_mean` may be uninitialized [unbound-name]
- ERROR sklearn/linear_model/_coordinate_descent.py:184:34-40: `X_mean` may be uninitialized [unbound-name]
- ERROR sklearn/linear_model/_huber.py:66:24-33: `intercept` may be uninitialized [unbound-name]
- ERROR sklearn/linear_model/_least_angle.py:670:17-22: `coefs` may be uninitialized [unbound-name]
+ ERROR sklearn/linear_model/_least_angle.py:670:33-37: `coef` may be uninitialized [unbound-name]
- ERROR sklearn/linear_model/_least_angle.py:835:26-31: `coefs` may be uninitialized [unbound-name]
- ERROR sklearn/linear_model/_least_angle.py:836:46-55: `prev_coef` may be uninitialized [unbound-name]
- ERROR sklearn/linear_model/_least_angle.py:841:36-42: `alphas` may be uninitialized [unbound-name]
+ ERROR sklearn/linear_model/_least_angle.py:907:17-22: `coefs` may be uninitialized [unbound-name]
+ ERROR sklearn/linear_model/_least_angle.py:915:35-39: `coef` may be uninitialized [unbound-name]
+ ERROR sklearn/linear_model/_least_angle.py:917:35-39: `coef` may be uninitialized [unbound-name]
- ERROR sklearn/linear_model/_omp.py:139:13-18: `coefs` may be uninitialized [unbound-name]
- ERROR sklearn/linear_model/_omp.py:147:43-48: `coefs` may be uninitialized [unbound-name]
- ERROR sklearn/linear_model/_omp.py:272:13-18: `coefs` may be uninitialized [unbound-name]
- ERROR sklearn/linear_model/_omp.py:285:43-48: `coefs` may be uninitialized [unbound-name]
- ERROR sklearn/linear_model/_ridge.py:277:26-28: `sw` may be uninitialized [unbound-name]
- ERROR sklearn/linear_model/_ridge.py:294:27-29: `sw` may be uninitialized [unbound-name]
- ERROR sklearn/linear_model/tests/test_base.py:834:41-50: `intercept` may be uninitialized [unbound-name]
- ERROR sklearn/linear_model/tests/test_base.py:866:41-52: `intercept_0` may be uninitialized [unbound-name]
- ERROR sklearn/linear_model/tests/test_coordinate_descent.py:1294:41-50: `intercept` may be uninitialized [unbound-name]
- ERROR sklearn/linear_model/tests/test_coordinate_descent.py:1326:41-52: `intercept_0` may be uninitialized [unbound-name]
- ERROR sklearn/linear_model/tests/test_coordinate_descent.py:1521:41-50: `intercept` may be uninitialized [unbound-name]
- ERROR sklearn/linear_model/tests/test_ridge.py:2415:41-50: `intercept` may be uninitialized [unbound-name]
- ERROR sklearn/model_selection/_validation.py:421:24-41: `train_scores_dict` may be uninitialized [unbound-name]
- ERROR sklearn/neighbors/_base.py:382:16-26: `neigh_dist` may be uninitialized [unbound-name]
- ERROR sklearn/preprocessing/_data.py:266:19-24: `mean_` may be uninitialized [unbound-name]
- ERROR sklearn/preprocessing/_data.py:283:45-51: `scale_` may be uninitialized [unbound-name]

pwndbg (https://github.com/pwndbg/pwndbg)
- ERROR pwndbg/aglib/nearpc.py:610:17-25: `pair_map` may be uninitialized [unbound-name]
- ERROR pwndbg/aglib/nearpc.py:610:27-34: `pair_id` may be uninitialized [unbound-name]
- ERROR pwndbg/aglib/nearpc.py:610:36-51: `maximum_pair_id` may be uninitialized [unbound-name]

setuptools (https://github.com/pypa/setuptools)
- ERROR setuptools/_vendor/autocommand/autoasync.py:140:43-50: `new_sig` may be uninitialized [unbound-name]

manticore (https://github.com/trailofbits/manticore)
- ERROR manticore/core/smtlib/constraints.py:206:59-76: `constant_bindings` may be uninitialized [unbound-name]
+ ERROR manticore/core/smtlib/constraints.py:213:25-42: `constant_bindings` may be uninitialized [unbound-name]

mongo-python-driver (https://github.com/mongodb/mongo-python-driver)
- ERROR pymongo/asynchronous/pool.py:292:51-56: `start` may be uninitialized [unbound-name]
- ERROR pymongo/message.py:1142:39-53: `ns_doc_encoded` may be uninitialized [unbound-name]
- ERROR pymongo/pyopenssl_context.py:131:56-61: `start` may be uninitialized [unbound-name]
- ERROR pymongo/synchronous/pool.py:292:51-56: `start` may be uninitialized [unbound-name]

stone (https://github.com/dropbox/stone)
- ERROR stone/backends/python_rsrc/stone_base.py:19:9-15: `typing` may be uninitialized [unbound-name]

websockets (https://github.com/aaugustin/websockets)
- ERROR src/websockets/asyncio/connection.py:1219:17-27: `exceptions` may be uninitialized [unbound-name]
- ERROR src/websockets/asyncio/connection.py:1235:17-27: `exceptions` may be uninitialized [unbound-name]
- ERROR src/websockets/frames.py:261:37-47: `mask_bytes` may be uninitialized [unbound-name]
- ERROR src/websockets/frames.py:327:42-52: `mask_bytes` may be uninitialized [unbound-name]
- ERROR src/websockets/legacy/framing.py:102:37-46: `mask_bits` may be uninitialized [unbound-name]
- ERROR src/websockets/legacy/protocol.py:1610:17-27: `exceptions` may be uninitialized [unbound-name]
- ERROR src/websockets/legacy/protocol.py:1623:17-27: `exceptions` may be uninitialized [unbound-name]

zulip (https://github.com/zulip/zulip)
- ERROR zerver/lib/import_realm.py:1219:19-25: `bucket` may be uninitialized [unbound-name]
- ERROR zerver/lib/import_realm.py:1280:17-41: `filename_to_has_original` may be uninitialized [unbound-name]
- ERROR zerver/lib/streams.py:1421:51-69: `default_stream_ids` may be uninitialized [unbound-name]

static-frame (https://github.com/static-frame/static-frame)
- ERROR static_frame/core/db_util.py:576:32-44: `query_create` may be uninitialized [unbound-name]
- ERROR static_frame/core/frame.py:9574:37-48: `index_names` may be uninitialized [unbound-name]
- ERROR static_frame/core/frame.py:9578:46-57: `index_depth` may be uninitialized [unbound-name]
- ERROR static_frame/core/frame.py:9580:36-49: `columns_names` may be uninitialized [unbound-name]
- ERROR static_frame/core/frame.py:9583:55-66: `index_depth` may be uninitialized [unbound-name]
- ERROR static_frame/core/frame.py:9586:35-46: `filter_func` may be uninitialized [unbound-name]
- ERROR static_frame/core/frame.py:9600:24-35: `index_depth` may be uninitialized [unbound-name]
- ERROR static_frame/core/frame.py:9601:39-51: `index_values` may be uninitialized [unbound-name]
- ERROR static_frame/core/frame.py:9603:43-54: `filter_func` may be uninitialized [unbound-name]
- ERROR static_frame/core/frame.py:9607:44-56: `index_values` may be uninitialized [unbound-name]
- ERROR static_frame/core/frame.py:9609:47-58: `filter_func` may be uninitialized [unbound-name]
- ERROR static_frame/core/type_blocks.py:175:48-57: `drop_mask` may be uninitialized [unbound-name]
- ERROR static_frame/core/type_blocks.py:180:45-54: `drop_mask` may be uninitialized [unbound-name]
- ERROR static_frame/core/type_blocks.py:271:48-57: `drop_mask` may be uninitialized [unbound-name]
- ERROR static_frame/core/type_blocks.py:276:45-54: `drop_mask` may be uninitialized [unbound-name]
- ERROR static_frame/core/util.py:1398:20-24: `rows` may be uninitialized [unbound-name]
- ERROR static_frame/core/util.py:1403:21-28: `columns` may be uninitialized [unbound-name]
- ERROR static_frame/core/util.py:1405:21-28: `columns` may be uninitialized [unbound-name]
- ERROR static_frame/core/util.py:1415:22-26: `rows` may be uninitialized [unbound-name]
- ERROR static_frame/core/util.py:1415:28-35: `columns` may be uninitialized [unbound-name]
- ERROR static_frame/core/util.py:1552:27-38: `is_not_none` may be uninitialized [unbound-name]
- ERROR static_frame/core/util.py:1561:20-31: `is_not_none` may be uninitialized [unbound-name]
- ERROR static_frame/core/util.py:2031:13-24: `values_post` may be uninitialized [unbound-name]
- ERROR static_frame/core/util.py:2065:9-20: `values_post` may be uninitialized [unbound-name]
- ERROR static_frame/core/util.py:3140:40-44: `cols` may be uninitialized [unbound-name]
- ERROR static_frame/core/util.py:3147:40-44: `cols` may be uninitialized [unbound-name]
- ERROR static_frame/core/util.py:3175:48-52: `cols` may be uninitialized [unbound-name]
- ERROR static_frame/core/util.py:3208:61-65: `cols` may be uninitialized [unbound-name]

pandas (https://github.com/pandas-dev/pandas)
- ERROR pandas/core/common.py:603:32-41: `old_value` may be uninitialized [unbound-name]

mypy (https://github.com/python/mypy)
- ERROR mypy/test/testpep561.py:159:23-30: `program` may be uninitialized [unbound-name]
- ERROR mypyc/irbuild/ll_builder.py:2787:37-48: `false_block` may be uninitialized [unbound-name]

meson (https://github.com/mesonbuild/meson)
- ERROR mesonbuild/backend/ninjabackend.py:717:20-52: `captured_compile_args_per_target` may be uninitialized [unbound-name]
- ERROR mesonbuild/dependencies/dub.py:319:38-45: `winlibs` may be uninitialized [unbound-name]
- ERROR mesonbuild/modules/java.py:75:35-52: `sanitized_package` may be uninitialized [unbound-name]
- ERROR unittests/allplatformstests.py:5465:27-29: `cc` may be uninitialized [unbound-name]

core (https://github.com/home-assistant/core)
- ERROR homeassistant/components/bayesian/config_flow.py:557:25-34: `sub_entry` may be uninitialized [unbound-name]
- ERROR homeassistant/components/recorder/util.py:126:49-60: `timer_start` may be uninitialized [unbound-name]
- ERROR homeassistant/components/tplink/config_flow.py:459:27-29: `un` may be uninitialized [unbound-name]
- ERROR homeassistant/components/tplink/config_flow.py:459:34-36: `pw` may be uninitialized [unbound-name]
- ERROR homeassistant/components/tplink/entity.py:678:45-64: `has_parent_entities` may be uninitialized [unbound-name]
- ERROR homeassistant/helpers/entity.py:1375:17-28: `update_warn` may be uninitialized [unbound-name]

paasta (https://github.com/yelp/paasta)
- ERROR paasta_tools/utils.py:2880:29-36: `service` may be uninitialized [unbound-name]
- ERROR paasta_tools/utils.py:2882:31-40: `component` may be uninitialized [unbound-name]
- ERROR paasta_tools/utils.py:2883:27-35: `loglevel` may be uninitialized [unbound-name]
- ERROR paasta_tools/utils.py:2884:29-36: `cluster` may be uninitialized [unbound-name]
- ERROR paasta_tools/utils.py:2885:30-38: `instance` may be uninitialized [unbound-name]
- ERROR paasta_tools/utils.py:2893:25-32: `service` may be uninitialized [unbound-name]
- ERROR paasta_tools/utils.py:2895:27-36: `component` may be uninitialized [unbound-name]
- ERROR paasta_tools/utils.py:2896:23-31: `loglevel` may be uninitialized [unbound-name]
- ERROR paasta_tools/utils.py:2897:25-32: `cluster` may be uninitialized [unbound-name]
- ERROR paasta_tools/utils.py:2898:26-34: `instance` may be uninitialized [unbound-name]
- ERROR paasta_tools/utils.py:2910:13-22: `proctimer` may be uninitialized [unbound-name]

xarray (https://github.com/pydata/xarray)
+ ERROR xarray/plot/facetgrid.py:261:13-17: `nrow` may be uninitialized [unbound-name]
+ ERROR xarray/plot/facetgrid.py:262:13-17: `ncol` may be uninitialized [unbound-name]

spack (https://github.com/spack/spack)
- ERROR lib/spack/spack/config.py:728:34-42: `comments` may be uninitialized [unbound-name]

kornia (https://github.com/kornia/kornia)
- ERROR kornia/feature/disk/disk.py:117:39-40: `h` may be uninitialized [unbound-name]
- ERROR kornia/feature/disk/disk.py:117:43-44: `w` may be uninitialized [unbound-name]

spark (https://github.com/apache/spark)
- ERROR python/pyspark/pandas/indexes/base.py:1597:57-69: `sequence_col` may be uninitialized [unbound-name]
- ERROR python/pyspark/pandas/tests/indexes/test_indexing_loc.py:263:32-38: `psser1` may be uninitialized [unbound-name]
- ERROR python/pyspark/pandas/tests/indexes/test_indexing_loc.py:263:40-45: `pser1` may be uninitialized [unbound-name]
- ERROR python/pyspark/pandas/tests/indexes/test_indexing_loc.py:264:32-38: `psser2` may be uninitialized [unbound-name]
- ERROR python/pyspark/pandas/tests/indexes/test_indexing_loc.py:264:40-45: `pser2` may be uninitialized [unbound-name]
- ERROR python/pyspark/pandas/tests/indexes/test_indexing_loc.py:332:32-38: `psser1` may be uninitialized [unbound-name]
- ERROR python/pyspark/pandas/tests/indexes/test_indexing_loc.py:332:40-45: `pser1` may be uninitialized [unbound-name]
- ERROR python/pyspark/pandas/tests/indexes/test_indexing_loc.py:333:32-38: `psser2` may be uninitialized [unbound-name]
- ERROR python/pyspark/pandas/tests/indexes/test_indexing_loc.py:333:40-45: `pser2` may be uninitialized [unbound-name]
+ ERROR python/pyspark/tests/upstream/pyarrow/test_pyarrow_array_cast.py:659:21-23: `np` may be uninitialized [unbound-name]

prefect (https://github.com/PrefectHQ/prefect)
- ERROR src/prefect/cli/deployment.py:750:48-70: `parsed_interval_anchor` may be uninitialized [unbound-name]

@github-actions
Copy link
Copy Markdown

Primer Diff Classification

✅ 24 improvement(s) | ➖ 1 neutral | 25 project(s) total | +8, -129 errors

24 improvement(s) across openlibrary, cloud-init, egglog-python, rotki, pytest, cryptography, scikit-learn, pwndbg, setuptools, mongo-python-driver, stone, websockets, zulip, static-frame, pandas, mypy, meson, core, paasta, xarray, spack, kornia, spark, prefect.

Project Verdict Changes Error Kinds Root Cause
openlibrary ✅ Improvement -2 unbound-name set_initialization_guards()
cloud-init ✅ Improvement -1 unbound-name set_initialization_guards()
egglog-python ✅ Improvement -3 unbound-name set_initialization_guards()
rotki ✅ Improvement -4 unbound-name set_initialization_guards()
pytest ✅ Improvement -4 unbound-name satisfy_initialization_guard()
cryptography ✅ Improvement -2 unbound-name set_initialization_guards()
scikit-learn ✅ Improvement +4, -28 Removed false positive unbound-name errors (28) pyrefly/lib/binding/scope.rs
pwndbg ✅ Improvement -3 unbound-name set_initialization_guards()
setuptools ✅ Improvement -1 unbound-name set_initialization_guards()
manticore ➖ Neutral +1, -1 unbound-name satisfy_initialization_guard()
mongo-python-driver ✅ Improvement -4 unbound-name set_initialization_guards()
stone ✅ Improvement -1 unbound-name set_initialization_guards()
websockets ✅ Improvement -7 unbound-name satisfy_initialization_guard()
zulip ✅ Improvement -3 unbound-name set_initialization_guards()
static-frame ✅ Improvement -28 unbound-name set_initialization_guards()
pandas ✅ Improvement -1 unbound-name set_initialization_guards()
mypy ✅ Improvement -2 unbound-name set_initialization_guards()
meson ✅ Improvement -4 unbound-name set_initialization_guards()
core ✅ Improvement -6 unbound-name set_initialization_guards()
paasta ✅ Improvement -11 unbound-name satisfy_initialization_guard()
xarray ✅ Improvement +2 unbound-name pyrefly/lib/binding/scope.rs
spack ✅ Improvement -1 unbound-name set_initialization_guards()
kornia ✅ Improvement -2 unbound-name set_initialization_guards()
spark ✅ Improvement +1, -9 Removed false positive unbound-name errors (correlated guards) set_initialization_guards()
prefect ✅ Improvement -1 unbound-name set_initialization_guards()
Detailed analysis

✅ Improvement (24)

openlibrary (-2)

The removed errors were false positives. The code pattern is: if user: defines ia_itemname and s3_keys, then if not user or not ia_itemname or not s3_keys: uses them. Short-circuit evaluation of or ensures ia_itemname and s3_keys are only evaluated when user is truthy, meaning the defining block has already run. The PR adds flow-sensitive tracking of initialization guards to handle exactly this pattern, correctly eliminating these false positives.
Attribution: The changes in pyrefly/lib/binding/scope.rs and pyrefly/lib/binding/bindings.rs add initialization guard tracking. Specifically: FlowStyle::PossiblyUninitialized now carries an Option<Name> guard, set_initialization_guards() in scope.rs sets guards after non-exhaustive forks, and satisfy_initialization_guard() upgrades guarded names to FlowStyle::Other when the same condition variable is narrowed as truthy. This allows pyrefly to recognize that ia_itemname and s3_keys are initialized whenever user is truthy, eliminating the false positive unbound-name errors.

cloud-init (-1)

This is a clear improvement. The removed error was a false positive — confd_fn is defined inside if create_confd: (line 605) and only used inside if create_confd: (line 614), with create_confd unchanged between the two blocks. Pyrefly's new guard-tracking system correctly recognizes this correlated initialization pattern and suppresses the spurious unbound-name error.
Attribution: The PR adds initialization guard tracking to the flow system. Specifically, set_initialization_guards() in pyrefly/lib/binding/scope.rs detects that confd_fn is defined under if create_confd: and sets a guard linking confd_fn's initialization to create_confd's truthiness. Then satisfy_initialization_guard() in the same file recognizes that the later if create_confd: satisfies this guard, upgrading confd_fn's FlowStyle from PossiblyUninitialized(Some(create_confd)) to FlowStyle::Other, which suppresses the false positive unbound-name error.

egglog-python (-3)

All three removed errors are false positives. The egraphs variable is defined under if visualize: and only used under the same if visualize: guard, with visualize never modified in between. Pyrefly's new guard-predicate tracking correctly recognizes this pattern and suppresses the spurious unbound-name errors.
Attribution: The changes to set_initialization_guards() in pyrefly/lib/binding/scope.rs and satisfy_initialization_guard() in the same file implement tracking of guard predicates. When if visualize: egraphs = [...] is processed, the system now records that egraphs is guarded by visualize. When a subsequent if visualize: is encountered, satisfy_initialization_guard() upgrades egraphs from PossiblyUninitialized(Some(visualize)) to FlowStyle::Other, suppressing the false positive.

rotki (-4)

All four removed errors are false positives. The pattern is: a variable is assigned inside if X: and later used inside another if X: where X has not been modified between the two checks. The variable is guaranteed to be initialized at the use site. The PR correctly implements correlated-condition tracking to suppress these false positives. The # pyright: ignore comments in the source code confirm the project authors already knew these were false positives from type checkers.
Attribution: The changes in pyrefly/lib/binding/scope.rs and pyrefly/lib/binding/bindings.rs implement initialization guard tracking. Specifically:

  • FlowStyle::PossiblyUninitialized and FlowStyle::MaybeInitialized now carry an Option<Name> guard field
  • set_initialization_guards() in pyrefly/lib/binding/scope.rs sets guards after non-exhaustive forks when a variable is initialized under if a:
  • satisfy_initialization_guard() in pyrefly/lib/binding/scope.rs upgrades guarded names to FlowStyle::Other when the same condition is narrowed as truthy
  • invalidate_guards_for_potential_mutation() and invalidate_guards_for_condition_var() ensure guards are cleared when the condition variable might change
  • The guard_to_guarded reverse index in Flow enables efficient lookup of which names are guarded by a given condition variable

pytest (-4)

This is a clear improvement. The PR implements correlated-condition tracking for variable initialization. In summary_stats(), display_sep is set once and never modified. fullwidth is assigned only when display_sep is true, and every use of fullwidth is also guarded by if display_sep:. The old pyrefly couldn't correlate these conditions and falsely reported fullwidth as possibly uninitialized. The new flow analysis correctly recognizes the pattern and suppresses the false positive.
Attribution: The PR adds a guard-tracking system to pyrefly's flow analysis. Specifically, the changes in pyrefly/lib/binding/scope.rs (the FlowStyle::PossiblyUninitialized now carries an Option<Name> guard, and satisfy_initialization_guard() upgrades guarded names to FlowStyle::Other when the same condition is seen again) and pyrefly/lib/binding/bindings.rs (the set_initialization_guards() method and the call to satisfy_initialization_guard() during narrowing) allow pyrefly to recognize that when fullwidth is defined inside if display_sep: and later used inside another if display_sep:, the variable is guaranteed to be initialized. The guard display_sep is tracked, and when the same truthiness check is encountered, the PossiblyUninitialized status is cleared.

cryptography (-2)

These are clear false positives being removed. In _load_ssh_public_identity(), the variable with_cert is a local boolean set once (lines 1044-1046) and never modified. cert_body (line 1060) and nonce (line 1065) are both assigned inside if with_cert: blocks, and both are used inside a later if with_cert: block (line 1067). The PR's new guard-tracking system correctly recognizes that when the same unmodified condition guards both the definition and the use, the variable is guaranteed to be initialized. This is exactly the pattern described in the PR description.
Attribution: The PR adds initialization guard tracking to the flow system in pyrefly/lib/binding/scope.rs and pyrefly/lib/binding/bindings.rs. Specifically, FlowStyle::PossiblyUninitialized and FlowStyle::MaybeInitialized now carry an optional guard (Option<Name>) that records the condition variable under which a name was initialized. The set_initialization_guards() method in bindings.rs sets these guards after non-exhaustive forks (like if with_cert: cert_body = rest). The satisfy_initialization_guard() method upgrades guarded names to FlowStyle::Other (initialized) when the same condition variable is narrowed as truthy in a subsequent if. This directly fixes the false positives where cert_body and nonce were flagged as possibly uninitialized despite being guarded by the same with_cert condition.

scikit-learn (+4, -28)

Removed false positive unbound-name errors (28): These were false positives where variables defined inside if a: were flagged as possibly uninitialized when used inside a subsequent if a: with the same unmodified condition. The PR's guard tracking correctly identifies these as initialized.
New false positive unbound-name errors (4): These are false positives in _lars_path_solver where coef, coefs, alpha are conditionally defined based on return_path (lines 592-603: coefs/alphas defined when True, coef/alpha defined when False) and used in matching branches (lines 904-917). The variables are always defined on the paths where they're used. However, the complex control flow involving a while True loop with reassignments (line 653), a del statement (line 836), and multiple break/continue paths makes it difficult for the guard tracking system to prove initialization. Pyright also flags these 4 cases (4/4 per cross-check), confirming this is a known hard pattern for static flow analysis. Net effect is still strongly positive (28 removed - 4 added = 24 fewer FPs).

Overall: This is a clear improvement. The PR removes 28 false positives by tracking initialization guards (variables defined under if a: are known to be initialized when a is later tested as truthy). The 4 new errors are false positives in _lars_path_solver where variables are conditionally defined based on return_path and used in matching branches.

Looking at the code structure: at lines 592-603, coefs and alphas are defined when return_path is True, while coef, prev_coef, alpha, prev_alpha are defined when return_path is False. Later, at lines 904-917, the code checks if return_path: and uses coefs/alphas in the True branch and alpha/coef in the False branch. These variables are always defined on the paths where they're used.

The complication is the while True loop (line 637). Inside the loop at line 653, if return_path: reassigns alpha, coef, prev_alpha, prev_coef from alphas/coefs arrays. The loop also has a del coef, alpha, prev_alpha, prev_coef at line 836 followed by reassignment. This complex control flow inside the loop makes it difficult for the type checker's guard tracking to prove that the variables are always initialized on the correct paths.

The 4 new errors are false positives because the code correctly ensures each variable is defined before use on every execution path. The net effect is strongly positive: 28 FPs removed vs 4 FPs added, for a net reduction of 24 false positives. Pyright also flags these 4 cases (4/4 per cross-check data), confirming this is a known hard pattern for static flow analysis.

Attribution: The changes in pyrefly/lib/binding/scope.rs and pyrefly/lib/binding/bindings.rs add initialization guard tracking. When a variable is defined inside if a:, the system now records that the variable's initialization is guarded by a. When a subsequent if a: is encountered, the guard is satisfied and the variable is treated as initialized. This directly causes the 28 false positive removals. The 4 new errors appear to be cases where the new system doesn't help — the return_path parameter-based branching pattern (if return_path: coefs = ...; else: coef = ...) followed by if return_path: ... coefs ... doesn't match the simple if a: guard pattern because the condition variable return_path is used in the while loop body in ways that may confuse the guard tracking (e.g., return_path is read at line 653 inside the loop, which triggers invalidate_guards_for_potential_mutation via the lookup_name path since it's not a narrowing use).

pwndbg (-3)

The PR adds flow-sensitive tracking of initialization guards for simple predicates. In the pwndbg code, branch_visualization is a function parameter checked at line 493 (where the three variables are defined) and again at line 608 (where they are used). Since branch_visualization is never reassigned between these points, the variables are guaranteed to be initialized whenever line 610 executes. The previous unbound-name errors were false positives, and removing them is an improvement.
Attribution: The changes to pyrefly/lib/binding/scope.rs and pyrefly/lib/binding/bindings.rs add initialization guard tracking. Specifically: (1) FlowStyle::PossiblyUninitialized and FlowStyle::MaybeInitialized now carry an optional guard name (Option<Name>), (2) set_initialization_guards() in scope.rs sets guards after non-exhaustive forks when a bare truthiness condition (if a:) is detected, (3) satisfy_initialization_guard() upgrades guarded names to FlowStyle::Other when the same condition is narrowed again (i.e., entering another if a: block). This directly fixes the false positive: after if branch_visualization: pair_map, pair_id, maximum_pair_id = ..., the guard branch_visualization is set on those three variables. When the later if branch_visualization: is encountered, satisfy_initialization_guard() upgrades their flow style, suppressing the unbound-name errors.

setuptools (-1)

This is a clear false positive removal. The variable new_sig is defined on line 110 inside if pass_loop: and used on line 140 inside another if pass_loop:. Since pass_loop is never modified between these two checks, new_sig is guaranteed to be initialized at the point of use. The PR's new guard-tracking system correctly recognizes this pattern and suppresses the spurious unbound-name error.
Attribution: The PR adds initialization guard tracking to pyrefly's flow system. Specifically, set_initialization_guards() in pyrefly/lib/binding/scope.rs now records that when a variable is defined inside if a:, it carries a guard linking it to condition variable a. Then satisfy_initialization_guard() upgrades the variable to FlowStyle::Other (initialized) when a subsequent if a: is encountered with the same unmodified condition. This directly fixes the false positive on new_sig in autoasync.py, where pass_loop is the guard condition.

mongo-python-driver (-4)

All four removed errors are false positives where pyrefly previously could not track that a variable defined inside if X: is guaranteed to be initialized when used inside a subsequent if X: with the same unmodified condition. The PR adds flow-sensitive guard tracking to handle this common pattern correctly, eliminating these spurious unbound-name errors.
Attribution: The changes in pyrefly/lib/binding/scope.rs and pyrefly/lib/binding/bindings.rs implement initialization guard tracking. Specifically: set_initialization_guards() in scope.rs records that when a variable is defined inside if a:, it carries a guard linking it to condition variable a. satisfy_initialization_guard() upgrades the guarded variable's FlowStyle to Other (initialized) when a subsequent if a: (truthy narrowing) is encountered. The FlowStyle::PossiblyUninitialized and FlowStyle::MaybeInitialized variants were extended with an Option<Name> guard field. The invalidate_guards_for_potential_mutation() and invalidate_guards_for_condition_var() methods ensure soundness by clearing guards when the condition variable might be mutated.

stone (-1)

This is a clear false positive removal. The pattern _MYPY = False; if _MYPY: import typing; ...; if _MYPY: T = typing.TypeVar(...) guarantees that typing is bound whenever it's used. The PR correctly tracks correlated conditions to avoid reporting typing as uninitialized.
Attribution: The PR adds initialization guard tracking to the flow system. Specifically, set_initialization_guards() in pyrefly/lib/binding/scope.rs now records that when a variable is defined inside if a:, it is guarded by a. Then satisfy_initialization_guard() upgrades the variable to FlowStyle::Other (initialized) when a subsequent if a: is encountered. This directly fixes the false positive: typing is defined under if _MYPY: and used under if _MYPY:, so the guard is satisfied and the unbound-name error is suppressed.

websockets (-7)

All 7 removed errors are false positives. The broadcast() function in connection.py defines exceptions: list[Exception] = [] on line 1208 inside if raise_exceptions:, then appends to exceptions on lines 1219 and 1235, both also inside if raise_exceptions:. Since raise_exceptions is a function parameter that is never modified, exceptions is always initialized when those lines execute. The PR correctly teaches pyrefly to track this correlation between the condition variable and the guarded variable's initialization status.
Attribution: The PR adds initialization guard tracking to the flow system. Key changes:

  • pyrefly/lib/binding/scope.rs: FlowStyle::PossiblyUninitialized and FlowStyle::MaybeInitialized now carry an optional guard (Option<Name>) identifying the condition variable under which the name IS initialized.
  • pyrefly/lib/binding/scope.rs: New guard_to_guarded map in Flow tracks which names are guarded by which condition variables.
  • pyrefly/lib/binding/scope.rs: satisfy_initialization_guard() upgrades guarded names to FlowStyle::Other (initialized) when the same condition is narrowed as truthy.
  • pyrefly/lib/binding/bindings.rs: bind_narrow_ops now calls satisfy_initialization_guard() when narrowing occurs, and lookup_name() invalidates guards on non-narrowing uses (conservative mutation handling).
  • pyrefly/lib/binding/scope.rs: set_initialization_guards() sets guards after non-exhaustive forks when the pattern if a: b = 3 is detected.

zulip (-3)

All three removed errors are false positives where pyrefly incorrectly reported variables as potentially uninitialized. For bucket (line 1219): it is assigned inside if s3_uploads: at line 1165-1170, and used inside another if s3_uploads: at line 1218. Since s3_uploads is not modified between these two checks, the variable is guaranteed to be initialized when used. For filename_to_has_original (line 1280): it is assigned inside if processing_emojis: at lines 1137-1150, and used at line 1280 inside a code path that is only reached when processing_emojis is true (the code reaches line 1280 through if processing_avatars or processing_emojis: at line 1271, then the else branch of if processing_avatars: at line 1272, which includes assert processing_emojis at line 1275, guaranteeing the variable was initialized). The third error regarding default_stream_ids in zerver/lib/streams.py follows a similar pattern. The PR adds flow-sensitive analysis to recognize these patterns and suppress the spurious warnings. This is a clear improvement in pyrefly's analysis precision.
Attribution: The changes in pyrefly/lib/binding/scope.rs and pyrefly/lib/binding/bindings.rs implement initialization guard tracking. Specifically:

  • FlowStyle::PossiblyUninitialized and FlowStyle::MaybeInitialized now carry an optional guard (Option<Name>) identifying the condition variable under which the name is initialized.
  • set_initialization_guards() in pyrefly/lib/binding/bindings.rs sets these guards after non-exhaustive forks (e.g., if a: b = 3).
  • satisfy_initialization_guard() in pyrefly/lib/binding/scope.rs upgrades guarded names to FlowStyle::Other (fully initialized) when the same condition variable is narrowed as truthy in a subsequent if a: block.
  • invalidate_guards_for_potential_mutation() and invalidate_guards_for_condition_var() ensure guards are cleared when the condition variable is reassigned, deleted, or potentially mutated.

These changes directly eliminate the false positive unbound-name errors for the pattern where a variable is defined under if x: and used under a subsequent if x: with x unchanged.

static-frame (-28)

All 28 removed errors are false positives. The PR correctly implements correlated-condition tracking: when a variable is defined under if a: and used under a subsequent if a: with no modification of a in between, the variable is guaranteed to be initialized. This is a well-known flow analysis improvement that mypy and pyright already handle. The PR also includes proper invalidation when the guard variable is reassigned, deleted, or potentially mutated (via method calls or augmented assignment), ensuring soundness.
Attribution: The PR adds initialization guard tracking to the flow system in pyrefly/lib/binding/scope.rs and pyrefly/lib/binding/bindings.rs. Specifically: (1) FlowStyle::PossiblyUninitialized and FlowStyle::MaybeInitialized now carry an optional guard (Option<Name>) identifying the condition variable under which the name IS initialized. (2) set_initialization_guards() in bindings.rs sets these guards after non-exhaustive forks when the condition is a bare truthiness check (if a:). (3) satisfy_initialization_guard() in scope.rs upgrades guarded names to FlowStyle::Other (fully initialized) when the same condition variable is narrowed as truthy. (4) invalidate_guards_for_potential_mutation() and invalidate_guards_for_condition_var() conservatively clear guards when the condition variable might be mutated. This directly eliminates the false positive unbound-name errors for the if a: b = 3; ...; if a: use(b) pattern.

pandas (-1)

This is a clear improvement. The temp_setattr context manager in pandas defines old_value = getattr(obj, attr) inside if condition: and uses it inside if condition: in the finally block. Since condition is never reassigned, old_value is always initialized when used. The old error was a false positive that the PR's new guard-tracking system correctly eliminates.
Attribution: The PR adds initialization guard tracking to pyrefly's flow system. Specifically, set_initialization_guards() in pyrefly/lib/binding/scope.rs detects that when old_value is defined inside if condition:, it gets a guard of condition. Then satisfy_initialization_guard() recognizes that the subsequent if condition: satisfies that guard, upgrading old_value's flow style from PossiblyUninitialized(Some(condition)) to FlowStyle::Other (fully initialized). This eliminates the false positive.

mypy (-2)

Both removed errors are clear false positives. The PR adds flow-sensitive tracking of correlated conditions: when a variable is defined inside if cond: and later used inside another if cond: where cond hasn't been modified, pyrefly now correctly recognizes the variable is initialized. This matches the behavior of mypy and pyright, which don't flag these patterns.
Attribution: The changes in pyrefly/lib/binding/scope.rs and pyrefly/lib/binding/bindings.rs add initialization guard tracking to the flow system. Specifically, FlowStyle::PossiblyUninitialized and FlowStyle::MaybeInitialized now carry an optional guard name (Option<Name>). The new set_initialization_guards() method in bindings.rs sets guards after non-exhaustive forks (e.g., if a: b = 3 sets a guard on b with condition a). The satisfy_initialization_guard() method in scope.rs upgrades guarded names to FlowStyle::Other when the same condition is narrowed again (e.g., a subsequent if a: satisfies the guard). This directly eliminates the false positives where variables defined under if X: were flagged as uninitialized when used under a subsequent if X:.

meson (-4)

All four removed errors are false positives where pyrefly incorrectly reported variables as potentially uninitialized. The PR adds flow-sensitive tracking of initialization guards — when a variable is defined inside a conditional block and later used in a context where the same condition guarantees the variable was initialized, pyrefly now correctly recognizes the variable is guaranteed to be initialized.

Specifically:

  • captured_compile_args_per_target (ninjabackend.py:717): Defined inside if capture: at line 663, used inside if capture: at line 716. Since capture is not modified between these points, the variable is guaranteed to be initialized.
  • winlibs (dub.py:319): Defined inside if is_windows: at line 302-304, used at line 319 in if is_windows and lib in winlibs:. Due to Python's short-circuit evaluation of and, winlibs is only accessed when is_windows is True, which guarantees it was defined.
  • sanitized_package (java.py:75): Defined inside a conditional and used inside the same or equivalent guard condition.
  • cc (allplatformstests.py:5465): Defined inside a conditional and used inside the same or equivalent guard condition.

This is a clear improvement in analysis precision, correctly handling the common Python pattern of guarding variable usage with the same condition under which the variable was defined.

Attribution: The changes in pyrefly/lib/binding/scope.rs and pyrefly/lib/binding/bindings.rs implement initialization guard tracking. Specifically:

  • FlowStyle::PossiblyUninitialized and FlowStyle::MaybeInitialized now carry an optional guard (Option<Name>) identifying the condition variable.
  • set_initialization_guards() in pyrefly/lib/binding/scope.rs sets guards after non-exhaustive forks when a bare truthiness condition (if a:) is detected.
  • satisfy_initialization_guard() in pyrefly/lib/binding/scope.rs upgrades guarded names to FlowStyle::Other (initialized) when the same condition is narrowed as truthy again.
  • invalidate_guards_for_potential_mutation() and invalidate_guards_for_condition_var() ensure guards are cleared when the condition variable might be mutated.
    These changes directly cause the four false positive unbound-name errors to be suppressed when the same condition guards both definition and use.

core (-6)

All 6 removed errors are false positives where pyrefly incorrectly reported variables as possibly uninitialized. The variables are defined inside if condition: blocks and used inside subsequent if condition: blocks with the same unmodified condition, guaranteeing initialization. The PR correctly implements guard tracking to suppress these false positives while maintaining soundness (guards are invalidated when the condition variable is modified).
Attribution: The PR adds initialization guard tracking to the flow system in pyrefly/lib/binding/scope.rs and pyrefly/lib/binding/bindings.rs. Specifically:

  • FlowStyle::PossiblyUninitialized and FlowStyle::MaybeInitialized now carry an Option<Name> guard field tracking which condition variable guarantees initialization.
  • set_initialization_guards() in pyrefly/lib/binding/scope.rs sets guards after non-exhaustive forks when the pattern if a: b = ... is detected.
  • satisfy_initialization_guard() in pyrefly/lib/binding/scope.rs upgrades guarded names to FlowStyle::Other (initialized) when a matching if a: narrowing is encountered.
  • invalidate_guards_for_potential_mutation() and invalidate_guards_for_condition_var() ensure guards are cleared when the condition variable might be modified.

This directly addresses the false positive pattern where if a: b = val; ...; if a: use(b) was incorrectly flagged as b possibly uninitialized.

paasta (-11)

This is a clear improvement. The PR targets false positive unbound-name errors where a variable is defined inside an if condition: block and used inside a subsequent if condition: block with the same unmodified condition. The 11 removed errors in paasta_tools/utils.py follow this pattern in the _run function. Specifically, variables service, component, cluster, instance, and loglevel are assigned inside if log: (line 2843-2848) and then used inside subsequent if log: guards (lines 2878 and 2891). Similarly, proctimer is assigned inside if timeout: (line 2869-2871) and used inside subsequent if timeout: guards (lines 2904-2905 and 2909-2910). Since log and timeout are function parameters that are not reassigned between these guards, the variables are guaranteed to be bound whenever the subsequent guards are entered. These were false positives that pyrefly now correctly handles.
Attribution: The PR adds a guard-tracking system to pyrefly's flow analysis. Specifically, changes in pyrefly/lib/binding/scope.rs add guard_to_guarded tracking to Flow, and FlowStyle::PossiblyUninitialized and FlowStyle::MaybeInitialized now carry an optional guard name. The satisfy_initialization_guard() method in pyrefly/lib/binding/scope.rs upgrades guarded names to FlowStyle::Other when the same condition variable is narrowed as truthy. The set_initialization_guards() method in pyrefly/lib/binding/scope.rs sets guards after non-exhaustive forks. Together, these changes allow pyrefly to recognize that if a: b = 3; ...; if a: use(b) is safe when a is unchanged.

xarray (+2)

These are false positives (regressions). The variables nrow and ncol are always initialized before use at lines 261-262. The PR's changes to the flow system introduced new PossiblyUninitialized tracking that doesn't handle the complex correlated-condition pattern in xarray's FacetGrid.init. Before this PR, pyrefly did not report these errors. The PR author even has a test case test_guarded_initialization_with_maybe_initialized marked as bug = "terminated branch in nested fork loses termination info for MaybeInitialized" that mimics this exact xarray pattern, acknowledging it's a known limitation.
Attribution: The PR restructured FlowStyle::PossiblyUninitialized and FlowStyle::MaybeInitialized in pyrefly/lib/binding/scope.rs to carry optional guard information, and changed the merge logic in pyrefly/lib/binding/scope.rs (the merge_flow and set_initialization_guards functions). The new guard system only handles simple if a: predicates. The xarray pattern involves a multi-way if/elif/else where single_group is set to False in one branch and truthy values in others, with nrow/ncol set in different branches. The merge logic likely now produces PossiblyUninitialized(None) for nrow/ncol after the first if/elif/else, and the subsequent if single_group: block's guard satisfaction doesn't help because the correlation is between single_group being falsy (branch 1 already set them) vs truthy (this block sets them). This is a regression introduced by the restructured flow styles.

spack (-1)

This is a clear improvement. The removed error was a false positive — comments is defined inside if need_comment_copy: on line 724 and only used inside if need_comment_copy and comments: on line 728. Since need_comment_copy is not modified between these two points, comments is always initialized when accessed. The PR's new guard-tracking system correctly recognizes this correlated-condition pattern and suppresses the spurious unbound-name error.
Attribution: The PR adds initialization guard tracking to the flow system. Specifically, set_initialization_guards() in pyrefly/lib/binding/scope.rs detects that comments is defined under if need_comment_copy: and sets a guard linking comments to need_comment_copy. Then satisfy_initialization_guard() recognizes that the subsequent if need_comment_copy satisfies that guard, upgrading comments from PossiblyUninitialized(Some(need_comment_copy)) to FlowStyle::Other (initialized). This directly eliminates the false positive.

kornia (-2)

The analysis is factually correct. Looking at the source code:

  1. On line 109, if pad_if_not_divisible: guards the assignment of h and w on line 110: h, w = images.shape[2:]
  2. On line 116, the same condition if pad_if_not_divisible: guards the use of h and w on line 117: heatmaps = heatmaps[..., :h, :w]
  3. The variable pad_if_not_divisible is a function parameter of type bool and is not modified between lines 109 and 116.
  4. Therefore, if execution reaches line 117, pad_if_not_divisible must be True, which means line 110 must have executed, and h and w are guaranteed to be bound.

These are indeed false positives. The if cond: x = val; ...; if cond: use(x) pattern with an unmodified condition is safe, and the analysis correctly identifies this. The claim about the PR implementing guard tracking to recognize this pattern and eliminate these false positives is consistent with the code structure.

Attribution: The PR adds initialization guard tracking to the flow system. Specifically, set_initialization_guards() in pyrefly/lib/binding/scope.rs records that when a variable is defined inside if a:, it carries a guard linking it to condition variable a. Then satisfy_initialization_guard() upgrades the guarded name to FlowStyle::Other (initialized) when a subsequent if a: (IsTruthy narrow) is encountered. This directly eliminates the false positive for h and w which are guarded by pad_if_not_divisible.

spark (+1, -9)

Removed false positive unbound-name errors (correlated guards): All 9 removed errors follow the pattern where a variable is defined inside if condition: and used inside a subsequent if condition: with the condition unmodified between the two blocks. For example, sequence_col in base.py is defined at line 1580 inside if return_indexer: and used at line 1597 inside the same if return_indexer: guard. Similarly, psser1/pser1 in test_indexing_loc.py are defined and used under the same if check_ser: guard. These were all false positives that the new guard tracking correctly eliminates.
New unbound-name error on np in decorator argument: The new error flags np at line 659 in a @unittest.skipIf decorator where np is used in LooseVersion(np.__version__). The np module is imported conditionally under if have_numpy: (line 79), and the decorator argument uses not have_numpy or LooseVersion(np.__version__) < ... which guarantees via short-circuit evaluation that np is only accessed when have_numpy is True. Pyright also flags this (1/1 cross-check), suggesting this is a known limitation of static analyzers rather than a pyrefly-specific regression. This is a minor false positive but is co-reported by pyright.

Overall: This is a net improvement. The PR removes 9 false positive unbound-name errors by implementing correlated initialization guard tracking — when a variable is defined under if a: and used under a subsequent if a: with a unmodified, pyrefly now correctly recognizes the variable as initialized. The 1 new error (np may be uninitialized) is a borderline case: np is imported under if have_numpy: and used in a decorator argument not have_pyarrow or not have_pandas or not have_numpy or LooseVersion(np.__version__) < ... where short-circuit evaluation guarantees np is available. However, pyright also flags this same pattern, and it's genuinely difficult for static analyzers to track initialization through short-circuit or chains in decorator arguments. The net effect is strongly positive: 9 clear false positives removed, 1 debatable new error added (co-reported by pyright).

Per-category reasoning:

  • Removed false positive unbound-name errors (correlated guards): All 9 removed errors follow the pattern where a variable is defined inside if condition: and used inside a subsequent if condition: with the condition unmodified between the two blocks. For example, sequence_col in base.py is defined at line 1580 inside if return_indexer: and used at line 1597 inside the same if return_indexer: guard. Similarly, psser1/pser1 in test_indexing_loc.py are defined and used under the same if check_ser: guard. These were all false positives that the new guard tracking correctly eliminates.
  • New unbound-name error on np in decorator argument: The new error flags np at line 659 in a @unittest.skipIf decorator where np is used in LooseVersion(np.__version__). The np module is imported conditionally under if have_numpy: (line 79), and the decorator argument uses not have_numpy or LooseVersion(np.__version__) < ... which guarantees via short-circuit evaluation that np is only accessed when have_numpy is True. Pyright also flags this (1/1 cross-check), suggesting this is a known limitation of static analyzers rather than a pyrefly-specific regression. This is a minor false positive but is co-reported by pyright.

Attribution: The changes in pyrefly/lib/binding/scope.rs and pyrefly/lib/binding/bindings.rs implement initialization guard tracking. Specifically: (1) FlowStyle::PossiblyUninitialized and FlowStyle::MaybeInitialized now carry an Option<Name> guard field. (2) set_initialization_guards() in bindings.rs sets guards after non-exhaustive forks when the condition is a bare truthiness check (if a:). (3) satisfy_initialization_guard() upgrades guarded names to FlowStyle::Other when the same condition is narrowed again. (4) invalidate_guards_for_potential_mutation() conservatively clears guards when the condition variable is used (non-narrowing use). The new error on np at line 659 occurs because the short-circuit or chain in the decorator argument doesn't create the same guard pattern — np is conditionally imported under if have_numpy: but the decorator's or chain doesn't establish a simple if have_numpy: guard that the new system can track.

prefect (-1)

This is a clear false positive removal. The pattern if interval_anchor: parsed_interval_anchor = ...; ...; if interval_anchor: use(parsed_interval_anchor) is safe because the same unmodified condition guards both definition and use. The PR correctly teaches pyrefly to track these correlated conditions, eliminating the spurious unbound-name error.
Attribution: The PR adds initialization guard tracking to pyrefly's flow system in pyrefly/lib/binding/scope.rs and pyrefly/lib/binding/bindings.rs. Specifically, FlowStyle::PossiblyUninitialized and FlowStyle::MaybeInitialized now carry an optional guard (Option<Name>) recording which condition variable guarantees initialization. The new set_initialization_guards() method in scope.rs sets these guards after non-exhaustive forks (like if a: b = 3), and satisfy_initialization_guard() upgrades guarded names to FlowStyle::Other (fully initialized) when the same condition is narrowed again (like a subsequent if a:). This directly fixes the false positive on parsed_interval_anchor because interval_anchor serves as the guard condition for both the definition and use sites.

➖ Neutral (1)

manticore (+1, -1)

This is a net improvement. The PR removes a false positive at line 206 and introduces an error at line 213, both involving constant_bindings potentially being uninitialized. Both are false positives — constant_bindings is always initialized when replace_constants is true, and both lines 206 and 213 are inside if replace_constants: guards.

Looking at the code structure in to_string() (line 179):

  • Line 182-190: if replace_constants: initializes constant_bindings = {}
  • Line 204-209: Inside a for-loop, if replace_constants: uses constant_bindings on line 206
  • Line 212-214: if replace_constants: uses constant_bindings on line 213

The old error was at line 206 (inside the for-loop's if replace_constants: block). The new error is at line 213 (inside a separate if replace_constants: block after the loop). Both are false positives since constant_bindings is guaranteed to be defined whenever replace_constants is true.

The PR's guard-tracking feature successfully resolves the false positive at line 206 by recognizing that the if replace_constants: check on line 205 ensures constant_bindings is initialized. However, the guard tracking does not successfully carry through to line 213's if replace_constants: check — likely because intervening control flow (the for-loop spanning lines 204-211, with its own conditional branches) causes the guard state to be reset or lost before reaching line 212.

The net effect is one false positive removed (line 206) and one false positive remaining at a different location (line 213). The error count stays at 1. Both errors are the same class of false positive (unbound-name for constant_bindings under a guard that guarantees initialization). The PR demonstrates partial progress on guard tracking — it handles the inner guard correctly but doesn't yet propagate the guard information across the loop boundary to the subsequent check.

Attribution: The PR adds initialization guard tracking in pyrefly/lib/binding/scope.rs (the satisfy_initialization_guard() method and set_initialization_guards() method). When pyrefly sees if replace_constants: constant_bindings = {} followed by if replace_constants: ... constant_bindings ..., the guard system now recognizes that constant_bindings is initialized whenever replace_constants is truthy. This removes the false positive at line 206. However, the guard at line 213 persists because the for loop on lines 204-211 iterates over constraints and uses constant_bindings inside if replace_constants: — the loop body's use of replace_constants as a narrowing condition may invalidate the guard (since lookup_name calls invalidate_guards_for_potential_mutation for non-narrowing uses of replace_constants). The reading of replace_constants on line 205 triggers guard invalidation, so by line 213 the guard is gone.


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (25 LLM)

jorenham added a commit to jorenham/pyrefly that referenced this pull request Apr 30, 2026
Co-authored-by: Zeina Migeed <migeedz@meta.com>
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.

1 participant