Skip to content

fix False positive [not-iterable] for dict literal with heterogeneous values built in a loop #2798#3317

Open
asukaminato0721 wants to merge 1 commit intofacebook:mainfrom
asukaminato0721:2798
Open

fix False positive [not-iterable] for dict literal with heterogeneous values built in a loop #2798#3317
asukaminato0721 wants to merge 1 commit intofacebook:mainfrom
asukaminato0721:2798

Conversation

@asukaminato0721
Copy link
Copy Markdown
Contributor

@asukaminato0721 asukaminato0721 commented May 6, 2026

Summary

Fixes #2798

dict literals ignore a bare unconstrained partial hint.

That lets the inner heterogeneous literal form an anonymous TypedDict first, then the surrounding assignment pins the partial container type to that precise result instead of collapsing it

Test Plan

add test

@meta-cla meta-cla Bot added the cla signed label May 6, 2026
@asukaminato0721 asukaminato0721 marked this pull request as ready for review May 6, 2026 14:40
Copilot AI review requested due to automatic review settings May 6, 2026 14:40
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a type inference edge case for dict literals used as values inside another dict that’s being built up in a loop, which previously could cause a false-positive [not-iterable] when accessing and iterating over a list stored under a different key.

Changes:

  • Ignore a bare, unconstrained partial type hint when inferring a dict literal, allowing heterogeneous inner dict literals to be inferred precisely (e.g., as an anonymous TypedDict-like shape) before being pinned by the outer assignment.
  • Add a regression test covering the reported loop-assigned heterogeneous inner dict scenario (Issue #2798).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
pyrefly/lib/alt/expr.rs Filters out a bare partial-hint during dict literal inference to avoid collapsing heterogeneous inner dict values incorrectly.
pyrefly/lib/test/dict.rs Adds a regression testcase reproducing the reported false-positive [not-iterable] scenario.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

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

jax (https://github.com/google/jax)
- ERROR jax/_src/internal_test_util/export_back_compat_test_data/cpu_triangular_solve_blas_trsm.py:114:26-151:2: Cannot set item in `dict[str, dict[str, bytes | date | int | list[str] | str | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]] | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]]]` [unsupported-operation]
- ERROR jax/_src/internal_test_util/export_back_compat_test_data/cpu_tridiagonal_solve_lapack_gtsv.py:158:26-233:2: Cannot set item in `dict[str, dict[str, bytes | date | int | list[str] | str | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]] | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]]]` [unsupported-operation]
- ERROR jax/_src/internal_test_util/export_back_compat_test_data/cuda_cholesky_solver_potrf.py:160:26-231:2: Cannot set item in `dict[str, dict[str, bytes | date | int | list[str] | str | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]]]` [unsupported-operation]
- ERROR jax/_src/internal_test_util/export_back_compat_test_data/cuda_eigh_cusolver_syev.py:183:26-271:2: Cannot set item in `dict[str, dict[str, bytes | date | int | list[str] | str | tuple[()] | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]]]` [unsupported-operation]
- ERROR jax/_src/internal_test_util/export_back_compat_test_data/cuda_lu_cusolver_getrf.py:115:26-157:2: Cannot set item in `dict[str, dict[str, bytes | date | int | list[str] | str | tuple[()] | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[signedinteger[_32Bit]]], ndarray[tuple[Any, ...], dtype[signedinteger[_32Bit]]]]]]` [unsupported-operation]
- ERROR jax/_src/internal_test_util/export_back_compat_test_data/cuda_qr_cusolver_geqrf.py:158:26-218:2: Cannot set item in `dict[str, dict[str, bytes | date | int | list[str] | str | tuple[()] | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]]]` [unsupported-operation]
- ERROR jax/_src/internal_test_util/export_back_compat_test_data/cuda_svd_cusolver_gesvd.py:187:36-283:2: Cannot set item in `dict[str, dict[str, bytes | date | int | list[str] | str | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]] | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]]]` [unsupported-operation]
- ERROR jax/_src/internal_test_util/export_back_compat_test_data/cuda_tridiagonal_cusolver_sytrd.py:182:26-269:2: Cannot set item in `dict[str, dict[str, bytes | date | int | list[str] | str | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]] | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]]]` [unsupported-operation]
- ERROR jax/_src/internal_test_util/export_back_compat_test_data/gpu_eigh_solver_syev.py:271:26-406:2: Cannot set item in `dict[str, dict[str, bytes | date | int | list[str] | str | tuple[()] | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]]]` [unsupported-operation]
- ERROR jax/_src/internal_test_util/export_back_compat_test_data/rocm_cholesky_solver_potrf.py:160:26-231:2: Cannot set item in `dict[str, dict[str, bytes | date | int | list[str] | str | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]]]` [unsupported-operation]
- ERROR jax/_src/internal_test_util/export_back_compat_test_data/rocm_lu_rocsolver_getrf.py:125:26-170:2: Cannot set item in `dict[str, dict[str, bytes | date | int | list[str] | str | tuple[()] | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[signedinteger[_32Bit]]], ndarray[tuple[Any, ...], dtype[signedinteger[_32Bit]]]]]]` [unsupported-operation]
- ERROR jax/_src/internal_test_util/export_back_compat_test_data/rocm_qr_hipsolver_geqrf.py:156:26-213:2: Cannot set item in `dict[str, dict[str, bytes | date | int | list[str] | str | tuple[()] | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]]]` [unsupported-operation]
- ERROR jax/_src/internal_test_util/export_back_compat_test_data/rocm_svd_hipsolver_gesvd.py:192:36-289:2: Cannot set item in `dict[str, dict[str, bytes | date | int | list[str] | str | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]] | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]]]` [unsupported-operation]
- ERROR jax/_src/internal_test_util/export_back_compat_test_data/rocm_tridiagonal_hipsolver_sytrd.py:178:26-262:2: Cannot set item in `dict[str, dict[str, bytes | date | int | list[str] | str | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]] | tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]]]` [unsupported-operation]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/cpu_triangular_solve_blas_trsm.py:119:12-125:73: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]], ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]]]` is not assignable to TypedDict key `inputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/cpu_triangular_solve_blas_trsm.py:126:22-133:65: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]]]` is not assignable to TypedDict key `expected_outputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/cpu_tridiagonal_solve_lapack_gtsv.py:163:12-189:55: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]], ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]], ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]], ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]]]` is not assignable to TypedDict key `inputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/cpu_tridiagonal_solve_lapack_gtsv.py:190:22-206:60: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]]]` is not assignable to TypedDict key `expected_outputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/cuda_cholesky_solver_potrf.py:165:12-172:77: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]]]` is not assignable to TypedDict key `inputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/cuda_cholesky_solver_potrf.py:173:22-181:25: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]]]` is not assignable to TypedDict key `expected_outputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/cuda_eigh_cusolver_syev.py:189:22-197:22: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` is not assignable to TypedDict key `expected_outputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/cuda_lu_cusolver_getrf.py:121:22-123:130: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]], ndarray[tuple[Any, ...], dtype[signedinteger[_32Bit]]], ndarray[tuple[Any, ...], dtype[signedinteger[_32Bit]]]]` is not assignable to TypedDict key `expected_outputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[signedinteger[_32Bit]]], ndarray[tuple[Any, ...], dtype[signedinteger[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/cuda_qr_cusolver_geqrf.py:164:22-178:24: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]], ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]]]` is not assignable to TypedDict key `expected_outputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/cuda_svd_cusolver_gesvd.py:192:12-209:25: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]]]` is not assignable to TypedDict key `inputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/cuda_svd_cusolver_gesvd.py:210:22-245:24: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]]]` is not assignable to TypedDict key `expected_outputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/cuda_tridiagonal_cusolver_sytrd.py:187:12-204:25: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]]]` is not assignable to TypedDict key `inputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/cuda_tridiagonal_cusolver_sytrd.py:205:22-227:51: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]]]` is not assignable to TypedDict key `expected_outputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/gpu_eigh_solver_syev.py:277:20-306:4: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` is not assignable to TypedDict key `expected_outputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/rocm_cholesky_solver_potrf.py:165:12-173:25: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]]]` is not assignable to TypedDict key `inputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/rocm_cholesky_solver_potrf.py:174:22-181:75: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]]]` is not assignable to TypedDict key `expected_outputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/rocm_lu_rocsolver_getrf.py:131:22-133:130: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]], ndarray[tuple[Any, ...], dtype[signedinteger[_32Bit]]], ndarray[tuple[Any, ...], dtype[signedinteger[_32Bit]]]]` is not assignable to TypedDict key `expected_outputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[signedinteger[_32Bit]]], ndarray[tuple[Any, ...], dtype[signedinteger[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/rocm_qr_hipsolver_geqrf.py:162:22-176:24: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]], ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]]]` is not assignable to TypedDict key `expected_outputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/rocm_svd_hipsolver_gesvd.py:197:12-214:25: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]]]` is not assignable to TypedDict key `inputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/rocm_svd_hipsolver_gesvd.py:215:22-250:24: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]]]` is not assignable to TypedDict key `expected_outputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/rocm_tridiagonal_hipsolver_sytrd.py:183:12-200:25: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]]]` is not assignable to TypedDict key `inputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]
+ ERROR jax/_src/internal_test_util/export_back_compat_test_data/rocm_tridiagonal_hipsolver_sytrd.py:201:22-224:51: `tuple[ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[complexfloating[_32Bit, _32Bit]]]]` is not assignable to TypedDict key `expected_outputs` with type `tuple[ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]], ndarray[tuple[Any, ...], dtype[floating[_32Bit]]]]` [bad-typed-dict-key]

cryptography (https://github.com/pyca/cryptography)
- ERROR tests/utils.py:415:34-60: Cannot set item in `dict[str, int | str]` [unsupported-operation]
- ERROR tests/utils.py:419:17-425:18: Argument `dict[str, bytes | int | str]` is not assignable to parameter `object` with type `dict[str, int | str]` in function `list.append` [bad-argument-type]
+ ERROR tests/utils.py:415:34-60: `bytes` is not assignable to TypedDict key `msg` with type `int | str` [bad-typed-dict-key]
+ ERROR tests/utils.py:424:28-54: `bytes` is not assignable to TypedDict key with type `int | str` [bad-typed-dict-key]

apprise (https://github.com/caronc/apprise)
- ERROR apprise/config/base.py:806:38-52: Cannot index into `int` [bad-index]
- ERROR apprise/config/base.py:806:38-52: Cannot index into `str` [bad-index]
- ERROR apprise/config/base.py:808:21-35: Cannot index into `int` [bad-index]
- ERROR apprise/config/base.py:808:21-35: Cannot index into `str` [bad-index]
- ERROR apprise/config/base.py:813:32-49: Cannot index into `int` [bad-index]
- ERROR apprise/config/base.py:813:32-49: Cannot index into `str` [bad-index]
- ERROR apprise/config/base.py:818:40-56: Cannot index into `int` [bad-index]
- ERROR apprise/config/base.py:818:40-56: Cannot index into `str` [bad-index]
- ERROR apprise/config/base.py:1324:38-52: Cannot index into `int` [bad-index]
- ERROR apprise/config/base.py:1326:21-35: Cannot index into `int` [bad-index]
- ERROR apprise/config/base.py:1332:32-49: Cannot index into `int` [bad-index]
- ERROR apprise/config/base.py:1337:40-56: Cannot index into `int` [bad-index]
+ ERROR apprise/plugins/pushover.py:430:17-434:18: Unpacked `dict[str, str | Unknown | None]` is not assignable to `dict[str, str | Unknown | None]` [bad-unpacking]
+ ERROR apprise/plugins/pushover.py:438:17-441:18: Unpacked `dict[str, str | Unknown | None]` is not assignable to `dict[str, str | Unknown | None]` [bad-unpacking]

zulip (https://github.com/zulip/zulip)
+ ERROR zerver/lib/dev_ldap_directory.py:42:65-47:14: Unpacked `dict[str, list[str]]` is not assignable to `dict[str, list[bytes] | list[str]]` [bad-unpacking]
+ ERROR zerver/lib/dev_ldap_directory.py:49:74-53:14: Unpacked `dict[str, list[str]]` is not assignable to `dict[str, list[bytes] | list[str]]` [bad-unpacking]
+ ERROR zerver/lib/dev_ldap_directory.py:55:74-57:14: Unpacked `dict[str, list[str]]` is not assignable to `dict[str, list[bytes] | list[str]]` [bad-unpacking]

pywin32 (https://github.com/mhammond/pywin32)
- ERROR win32/Demos/security/explicit_entries.py:128:26-35: Argument `list[dict[str, dict[str, Unknown | None] | int]]` is not assignable to parameter `obexpl_list` with type `tuple[dict[str, dict[str, PySID | int] | int], ...]` in function `_win32typing.PyACL.SetEntriesInAcl` [bad-argument-type]
+ ERROR win32/Demos/security/explicit_entries.py:128:26-35: Argument `list[dict[str, dict[str, Unknown | None] | int | Unknown]]` is not assignable to parameter `obexpl_list` with type `tuple[dict[str, dict[str, PySID | int] | int], ...]` in function `_win32typing.PyACL.SetEntriesInAcl` [bad-argument-type]
- ERROR win32/Demos/security/explicit_entries.py:165:26-35: Argument `list[dict[str, dict[str, Unknown | None] | int]]` is not assignable to parameter `obexpl_list` with type `tuple[dict[str, dict[str, PySID | int] | int], ...]` in function `_win32typing.PyACL.SetEntriesInAcl` [bad-argument-type]
+ ERROR win32/Demos/security/explicit_entries.py:165:26-35: Argument `list[dict[str, dict[str, Unknown | None] | int | Unknown]]` is not assignable to parameter `obexpl_list` with type `tuple[dict[str, dict[str, PySID | int] | int], ...]` in function `_win32typing.PyACL.SetEntriesInAcl` [bad-argument-type]

core (https://github.com/home-assistant/core)
- ERROR homeassistant/components/diagnostics/__init__.py:217:29-40: `list[dict[str, Any]]` is not assignable to TypedDict key `issues` with type `Mapping[str | None, dict[SetupPhases, float]] | Mapping[str, Any] | dict[str, dict[str, list[str] | str | None]] | dict[str, Any] | Manifest` [bad-typed-dict-key]
+ ERROR homeassistant/components/diagnostics/__init__.py:217:29-40: `list[dict[str, Any]]` is not assignable to TypedDict key `issues` with type `Mapping[str | None, dict[SetupPhases, float]] | Mapping[str, Any] | dict[str, dict[str, list[str] | str | Unknown | None]] | dict[str, Any] | Manifest` [bad-typed-dict-key]
- ERROR homeassistant/components/websocket_api/commands.py:1143:27-62: Cannot set item in `dict[str, dict[str, bool | None]]` [unsupported-operation]

spark (https://github.com/apache/spark)
- ERROR python/pyspark/pandas/tests/groupby/test_describe.py:34:22-76: Argument `dict[str, list[int] | list[str]]` is not assignable to parameter `object` with type `dict[str, list[int]]` in function `list.append` [bad-argument-type]
+ ERROR python/pyspark/pandas/tests/groupby/test_describe.py:34:28-43: `list[str]` is not assignable to TypedDict key `a` with type `list[int]` [bad-typed-dict-key]
- ERROR python/pyspark/pandas/tests/groupby/test_describe.py:70:22-82: Argument `dict[str, list[int] | list[str]]` is not assignable to parameter `object` with type `dict[str, list[str]]` in function `list.append` [bad-argument-type]
+ ERROR python/pyspark/pandas/tests/groupby/test_describe.py:70:50-59: `list[int]` is not assignable to TypedDict key `b` with type `list[str]` [bad-typed-dict-key]

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

Primer Diff Classification

❌ 2 regression(s) | ✅ 2 improvement(s) | ➖ 3 neutral | 7 project(s) total | +34, -30 errors

2 regression(s) across jax, zulip. error kinds: New bad-typed-dict-key errors (22 added), Removed unsupported-operation errors (14 removed), Spurious bad-unpacking on dict unpacking with heterogeneous values. 2 improvement(s) across apprise, core.

Project Verdict Changes Error Kinds Root Cause
jax ❌ Regression +22, -14 New bad-typed-dict-key errors (22 added) pyrefly/lib/alt/expr.rs
cryptography ➖ Neutral +2, -2 bad-argument-type, bad-typed-dict-key pyrefly/lib/alt/expr.rs
apprise ✅ Improvement +2, -8 bad-unpacking false positives (NEW) pyrefly/lib/alt/expr.rs
zulip ❌ Regression +3 Spurious bad-unpacking on dict unpacking with heterogeneous values pyrefly/lib/alt/expr.rs
pywin32 ➖ Neutral +2, -2 bad-argument-type
core ✅ Improvement +1, -2 bad-typed-dict-key, unsupported-operation pyrefly/lib/alt/expr.rs
spark ➖ Neutral +2, -2 New bad-typed-dict-key errors (false positives) pyrefly/lib/alt/expr.rs
Detailed analysis

❌ Regression (2)

jax (+22, -14)

New bad-typed-dict-key errors (22 added): These are false positives. The code uses dict() constructor calls (not TypedDict), and pyrefly incorrectly infers TypedDict-like structure for the values of the outer dict, then enforces key type constraints across heterogeneous assignments. Neither mypy nor pyright flag these (0/22 co-reported). The complexfloating vs floating mismatch is an artifact of pyrefly's overly precise type inference for plain dict constructor calls.
Removed unsupported-operation errors (14 removed): These were false positives. data_2025_10_20['c64'] = dict(...) is a valid dict item assignment. Removing 'Cannot set item in dict[...]' errors for plain dict assignments is correct — this is an improvement.

Overall: The PR fix for #2798 (dict literals ignoring unconstrained partial hints) has an unintended side effect on these JAX test data files. The pattern is: data = {} followed by multiple data['key'] = dict(...) assignments where the inner dict() calls have heterogeneous value types across different keys. Previously, pyrefly inferred a type for the outer dict that caused unsupported-operation errors when trying to assign values with complex array types. Now, pyrefly infers a more precise type for the outer dict based on the first assignment, treating the dict() result as having a TypedDict-like structure. When later assignments have values with complexfloating arrays instead of floating arrays, pyrefly flags them as bad-typed-dict-key because the value types don't match the shape inferred from the first assignment. Neither mypy nor pyright flag any of these — they correctly treat dict() as creating a plain dict, not a TypedDict. The net effect is trading 14 false positives for 22 different false positives, which is a regression in error count and error quality.

Attribution: The change in pyrefly/lib/alt/expr.rs in the hint.filter() line causes dict literals to ignore bare unconstrained partial hints. This means when processing data_2025_10_20['c64'] = dict(...), the dict literal dict(...) no longer gets constrained by the partial type inferred from earlier assignments (like data_2025_10_20['f32'] = dict(...)). Instead, pyrefly now infers an anonymous TypedDict for each dict() call. The first assignment ('f32') establishes a TypedDict type with inputs typed as tuple[ndarray[..., floating[_32Bit]], ...]. When the 'c64' assignment provides inputs with complexfloating[_32Bit, _32Bit] arrays, pyrefly sees a TypedDict key type mismatch because complexfloating is not assignable to floating. Previously, the partial hint would have collapsed the type to a plain dict union, avoiding this issue.

zulip (+3)

Spurious bad-unpacking on dict unpacking with heterogeneous values: Pyrefly infers a union value type list[bytes] | list[str] for the dict being constructed (due to mixing list[bytes] values like thumbnailPhoto with list[str] values), then incorrectly rejects unpacking common_data: dict[str, list[str]] into it. Since list[str] is assignable to list[bytes] | list[str] (as a member of the union), the unpacking is valid. The code is correct and neither mypy nor pyright flags it.

Overall: The PR's change to ignore bare partial hints for dict literals causes pyrefly to infer dict[str, list[bytes] | list[str]] for the dict being constructed via dict() with keyword arguments (since some values are list[bytes] like thumbnailPhoto/jpegPhoto and others are list[str]). It then incorrectly rejects unpacking **common_data (which is dict[str, list[str]]) into this type. This is a false positive because list[str] is assignable to list[bytes] | list[str] (it is a member of the union), so the unpacking is valid. Additionally, the function return type is dict[str, dict[str, Any]], so the precise inner dict value type is irrelevant. Neither mypy nor pyright flags this code.

Attribution: The change in pyrefly/lib/alt/expr.rs at the flatten_dict_items handling filters out bare unconstrained partial hints for dict literals. This causes pyrefly to infer a more precise heterogeneous type for the inner dict() call (e.g., dict[str, list[bytes] | list[str]] instead of collapsing to a simpler type). The stricter inferred type then triggers a spurious bad-unpacking error when **common_data (typed dict[str, list[str]]) is unpacked into the heterogeneous dict.

✅ Improvement (2)

apprise (+2, -8)

bad-unpacking false positives (NEW): The error message says a dict type is not assignable to itself (dict[str, str | Unknown | None] not assignable to dict[str, str | Unknown | None]). This is a self-contradictory error — a type should always be assignable to itself. The code simply unpacks base_payload into a new dict literal with additional string keys, which is perfectly valid. Neither mypy nor pyright flag this. This is a regression introduced by the PR's change to partial hint handling for dict literals.
bad-index false positives (REMOVED): The removed errors claimed "Cannot index into int" but the variable results at line 806 is clearly a dict (obtained from entry["results"]). Pyrefly was previously mis-inferring the type of values in the heterogeneous preloaded dict entries, collapsing them to int (from the "line" key's value). The PR fix correctly allows heterogeneous dict literals to maintain their precise types. Removing these false positives is an improvement.

Overall: Net assessment: 8 false positives removed, 2 false positives added. The removed errors were clearly wrong ("Cannot index into int" on a dict). The new errors are also wrong — {**base_payload, "user": self.user_key, "device": ...} is valid Python dict unpacking, and the error message "Unpacked dict[str, str | Unknown | None] is not assignable to dict[str, str | Unknown | None]" shows a type being flagged as incompatible with itself, which is nonsensical. Neither mypy nor pyright flag these. The net effect is positive (6 fewer false positives), but the 2 new errors are regressions.

Per-category reasoning:

  • bad-unpacking false positives (NEW): The error message says a dict type is not assignable to itself (dict[str, str | Unknown | None] not assignable to dict[str, str | Unknown | None]). This is a self-contradictory error — a type should always be assignable to itself. The code simply unpacks base_payload into a new dict literal with additional string keys, which is perfectly valid. Neither mypy nor pyright flag this. This is a regression introduced by the PR's change to partial hint handling for dict literals.
  • bad-index false positives (REMOVED): The removed errors claimed "Cannot index into int" but the variable results at line 806 is clearly a dict (obtained from entry["results"]). Pyrefly was previously mis-inferring the type of values in the heterogeneous preloaded dict entries, collapsing them to int (from the "line" key's value). The PR fix correctly allows heterogeneous dict literals to maintain their precise types. Removing these false positives is an improvement.

Attribution: The change in pyrefly/lib/alt/expr.rs in the AnswersSolver method adds a filter that causes dict literals to ignore a bare unconstrained partial hint: hint.filter(|hint| !matches!(hint.types(), [ty] if self.solver().is_partial(ty))). This improved inference for heterogeneous dict literals built in loops. For apprise/config/base.py, the preloaded list contains dicts like {"results": results, "line": line, "loggable_url": loggable_url} — a heterogeneous dict with mixed value types. Previously, the partial hint collapsed the type incorrectly (likely to int from the line value), causing false bad-index errors. The fix allows the dict to form a more precise type. However, the same change also caused new bad-unpacking false positives in pushover.py where base_payload (a dict[str, str | Unknown | None]) is unpacked — the new inference path apparently produces a spurious self-incompatibility error for dict unpacking.

core (+1, -2)

Net effect: one genuine false positive removed (commands.py unsupported-operation), and one existing false positive slightly worsened by introducing Unknown in its type description (diagnostics/init.py). The removal of the false positive is a clear improvement. The Unknown introduction is a minor regression in inference quality but doesn't change the error count. On balance, this is an improvement — one fewer false positive with only a cosmetic degradation on an already-existing false positive.
Attribution: The change in pyrefly/lib/alt/expr.rs in the dict expression handler adds a filter that ignores bare unconstrained partial hints when inferring dict literal types. This directly fixes the false positive at commands.py:1143 (the inner dict {"valid": False, "error": str(err)} now gets correct heterogeneous inference). It also causes the slight change in the diagnostics error message where Unknown appears in the type union, because the custom_components dict built in a loop now takes a different inference path.

➖ Neutral (3)

cryptography (+2, -2)

Net effect is roughly neutral: 2 old false positives removed, 2 new false positives introduced. Both old and new errors are false positives on valid Python code.

The code in load_fips_dsa_sig_vectors builds plain dict literals with heterogeneous value types — int values (from int(value, 16)), str values (from vectors[-1]["digest_algorithm"]), and bytes values (from binascii.unhexlify(hexmsg)). This is completely standard Python and is valid.

Old errors (removed):

  • unsupported-operation at line 415: pyrefly incorrectly inferred the dict's value type as int | str from earlier entries and then rejected assigning bytes to a key. This was a false positive — the dict is untyped (list[dict] return type uses bare dict).
  • bad-argument-type at line 419: a cascading false positive where pyrefly inferred the dict literal as dict[str, bytes | int | str] and rejected it as incompatible with the list's element type dict[str, int | str]. Also a false positive.

New errors (added):

  • bad-typed-dict-key at line 415 and 424: pyrefly now incorrectly applies TypedDict-like key type checking to plain dict literals, claiming bytes is not assignable to a TypedDict key msg with type int | str. These are plain dict objects, not TypedDicts, so this error category is misapplied. These are false positives.

The code is valid — binascii.unhexlify() returns bytes, and storing bytes values alongside int and str values in an untyped dict is perfectly fine. Neither mypy nor pyright flags these (consistent with the [pyrefly-only] tag).

Overall this is a wash: 2 false positives replaced by 2 different false positives. The new errors are arguably more confusing since they reference TypedDict semantics for plain dicts, though they are more localized than the old cascading bad-argument-type error.

Attribution: The change in pyrefly/lib/alt/expr.rs at the hint.filter(...) line causes pyrefly to ignore partial hints for dict literals, leading to anonymous TypedDict inference. This causes the dict at line 406 to be inferred as a TypedDict with fixed value types, which then rejects bytes values added later.

pywin32 (+2, -2)

Same errors at same locations with same error kinds — message wording changed, no behavioral impact.

spark (+2, -2)

New bad-typed-dict-key errors (false positives): Pyrefly now infers plain dict literals as anonymous TypedDicts due to the partial hint filtering change, then flags heterogeneous value types as TypedDict key violations. These are plain dicts, not TypedDicts. Neither mypy nor pyright flags these. The code is valid Python that runs correctly.
Removed bad-argument-type errors (false positives removed): The old errors incorrectly complained about appending heterogeneous dicts to a list. The list legitimately contains dicts with different value types. Removing these is an improvement.

Overall: The PR swaps one set of false positives for another. Previously, pyrefly inferred the dict literals as dict[str, list[int] | list[str]] and complained they couldn't be appended to a list typed as list[dict[str, list[int]]]. Now, pyrefly infers them as anonymous TypedDicts and complains that specific keys have the wrong type. Neither mypy nor pyright flags any of these lines. The code is straightforward: building a list of plain dicts with varying value types for test data. The new bad-typed-dict-key errors are false positives — these are plain dict literals, not TypedDicts. The removed bad-argument-type errors were also false positives. Net effect: 2 false positives removed, 2 different false positives added — a lateral move that is essentially neutral but slightly regressive because the new errors use TypedDict semantics on plain dicts, which is a more confusing error message.

Attribution: The change in pyrefly/lib/alt/expr.rs at the hint.filter(...) line causes dict literals to ignore bare unconstrained partial hints. This means that {"a": ["a", "a", "c"], "b": [4, 5, 6], "c": [7, 8, 9]} is now inferred as an anonymous TypedDict (with key a typed as list[str], b as list[int], c as list[int]) rather than being widened to dict[str, list[int] | list[str]]. When this TypedDict is appended to datas (which was previously typed from the first append as having all list[int] values), pyrefly now reports bad-typed-dict-key errors instead of bad-argument-type errors. The root problem shifted from one false positive flavor to another.

Suggested fixes

Summary: The PR's partial hint filter for dict literals causes pyrefly to infer anonymous TypedDicts for plain dict() calls and dict literals, producing 25+ false positive bad-typed-dict-key and bad-unpacking errors across jax, zulip, apprise, and other projects.

1. In the dict expression handler in pyrefly/lib/alt/expr.rs (around line 1008-1009), the new filter hint.filter(|hint| !matches!(hint.types(), [ty] if self.solver().is_partial(ty))) is too aggressive — it discards ALL partial hints for dict literals, which causes pyrefly to infer anonymous TypedDicts for plain dict literals and dict() constructor calls that should remain as regular dicts. The fix should be more targeted: only discard the partial hint when the dict literal is being assigned to a fresh variable (like bins = {}), not when it's being used as a value in a subscript assignment (like data['key'] = dict(...)) or as a dict constructor with keyword args. Specifically, instead of unconditionally filtering out all bare partial hints, add a condition that preserves the partial hint when the dict expression contains ** unpacking or when the hint comes from a subscript assignment context. Pseudo-code fix: let hint = hint.filter(|hint| !matches!(hint.types(), [ty] if self.solver().is_partial(ty) && flattened_items.iter().all(|item| !item.[is_unpacking()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/expr.rs)) && /* not in subscript-assignment context */)); Alternatively, a simpler and safer approach: only filter out the partial hint when the dict literal is empty (i.e., flattened_items.[is_empty()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/expr.rs)), since the original bug #2798 was about empty dict literals like bins = {} ignoring unconstrained partial hints. This would be: let hint = hint.filter(|hint| !matches!(hint.types(), [ty] if self.solver().is_partial(ty) && flattened_items.[is_empty()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/expr.rs)));

Files: pyrefly/lib/alt/expr.rs
Confidence: high
Affected projects: jax, zulip, apprise, cryptography, spark
Fixes: bad-typed-dict-key, bad-unpacking
The test case test_loop_assigned_heterogeneous_inner_dict shows the intended fix: bins = {} followed by bins[d] = {"start": d, "tasks": []} should work. The bug was that the empty dict bins = {} got a partial type that then over-constrained the inner dict literal. But the current fix filters out partial hints for ALL dict literals, not just empty ones. This causes: (1) In jax, data['f32'] = dict(...) infers an anonymous TypedDict instead of a plain dict, then data['c64'] = dict(...) with different value types triggers 22 bad-typed-dict-key errors. (2) In zulip, dict(**common_data, thumbnailPhoto=...) infers a heterogeneous union type and then rejects unpacking, causing 3 bad-unpacking errors. (3) In apprise, dict unpacking with {**base_payload, ...} triggers 2 spurious bad-unpacking errors. By restricting the filter to only apply when flattened_items.is_empty(), the original bug fix is preserved (empty dict literals won't be over-constrained by partial hints) while non-empty dict literals and dict() calls continue to receive proper hint propagation. This eliminates 22 bad-typed-dict-key errors in jax, 3 bad-unpacking errors in zulip, 2 bad-unpacking errors in apprise, and restores the 14 removed unsupported-operation errors in jax (which were being replaced by worse errors). It also fixes the neutral-classified regressions in cryptography and spark.


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (1 heuristic, 6 LLM)

@meta-codesync
Copy link
Copy Markdown
Contributor

meta-codesync Bot commented May 7, 2026

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

Copy link
Copy Markdown
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.

Copy link
Copy Markdown
Contributor

@rchen152 rchen152 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.

Copy link
Copy Markdown
Contributor

@rchen152 rchen152 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.

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.

False positive [not-iterable] for dict literal with heterogeneous values built in a loop

4 participants