Skip to content

[ty] Use TDD-based narrowing constraints and support NoReturn narrowing#23109

Merged
sharkdp merged 6 commits intoastral-sh:mainfrom
alex:alex/ty-narrowing-conditional-terminals
Feb 12, 2026
Merged

[ty] Use TDD-based narrowing constraints and support NoReturn narrowing#23109
sharkdp merged 6 commits intoastral-sh:mainfrom
alex:alex/ty-narrowing-conditional-terminals

Conversation

@alex
Copy link
Contributor

@alex alex commented Feb 6, 2026

Replace sorted-list-based narrowing constraints (AND-only) with TDD (Ternary Decision Diagram) nodes that support AND, OR, and NOT. This changes the merge operation from intersection to OR, so narrowing from non-terminal branches is properly preserved after if/elif/else.

Additionally, when a branch contains a NoReturn call (e.g. sys.exit()), gate the narrowing constraints by the ReturnsNever predicate. During the narrowing TDD walk, non-narrowing predicates are evaluated to determine reachability, so NoReturn branches contribute Never (absorbed by union) rather than polluting the merged type.

Fixes astral-sh/ty#690
Fixes astral-sh/ty#685

@alex
Copy link
Contributor Author

alex commented Feb 6, 2026

(Probably not ready for review yet, just wanted to see the ecosystem results.)

@alex alex force-pushed the alex/ty-narrowing-conditional-terminals branch from 6db3494 to 970a77d Compare February 6, 2026 01:41
@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Feb 6, 2026
@astral-sh-bot
Copy link

astral-sh-bot bot commented Feb 6, 2026

Typing conformance results

No changes detected ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Feb 6, 2026

mypy_primer results

Changes were detected when running on open source projects
anyio (https://github.com/agronholm/anyio)
- src/anyio/_backends/_trio.py:900:9: warning[possibly-missing-attribute] Attribute `send_nowait` may be missing on object of type `MemoryObjectSendStream[Unknown] | None`
- Found 89 diagnostics
+ Found 88 diagnostics

pyinstrument (https://github.com/joerick/pyinstrument)
- pyinstrument/__main__.py:377:27: error[no-matching-overload] No overload of function `dirname` matches arguments
- Found 42 diagnostics
+ Found 41 diagnostics

spack (https://github.com/spack/spack)
- lib/spack/spack/cmd/blame.py:246:30: error[invalid-argument-type] Argument to function `git_prefix` is incorrect: Expected `str | Path`, found `None | Unknown | str`
- lib/spack/spack/cmd/checksum.py:164:20: error[invalid-assignment] Object of type `dict[StandardVersion, str] | None` is not assignable to `dict[StandardVersion, str]`
- lib/spack/spack/cmd/patch.py:30:27: error[invalid-argument-type] Argument to function `_patch_env` is incorrect: Expected `Environment`, found `Environment | None`
- lib/spack/spack/cmd/repo.py:388:22: warning[possibly-missing-attribute] Attribute `package_api` may be missing on object of type `Repo | None`
- lib/spack/spack/cmd/repo.py:389:49: error[invalid-argument-type] Argument to function `migrate_v1_to_v2` is incorrect: Expected `Repo`, found `Repo | None`
- lib/spack/spack/cmd/repo.py:391:24: warning[possibly-missing-attribute] Attribute `package_api` may be missing on object of type `Repo | None`
- lib/spack/spack/cmd/repo.py:395:39: warning[possibly-missing-attribute] Attribute `packages_path` may be missing on object of type `Repo | None`
- lib/spack/spack/cmd/repo.py:395:59: warning[possibly-missing-attribute] Attribute `root` may be missing on object of type `Repo | None`
- lib/spack/spack/cmd/repo.py:408:45: warning[possibly-missing-attribute] Attribute `namespace` may be missing on object of type `Repo | None`
- lib/spack/spack/cmd/repo.py:416:28: warning[possibly-missing-attribute] Attribute `namespace` may be missing on object of type `Repo | None`
- lib/spack/spack/cmd/repo.py:423:28: warning[possibly-missing-attribute] Attribute `package_api_str` may be missing on object of type `Repo | None`
- lib/spack/spack/cmd/repo.py:427:50: warning[possibly-missing-attribute] Attribute `root` may be missing on object of type `Repo | None`
- lib/spack/spack/cmd/repo.py:432:33: warning[possibly-missing-attribute] Attribute `namespace` may be missing on object of type `Repo | None`
- lib/spack/spack/cmd/stage.py:77:27: error[invalid-argument-type] Argument to function `_stage_env` is incorrect: Expected `Environment`, found `Environment | None`
- lib/spack/spack/cmd/style.py:697:27: error[unresolved-attribute] Object of type `AST` has no attribute `lineno`
- lib/spack/spack/cmd/style.py:697:40: error[unresolved-attribute] Object of type `AST` has no attribute `col_offset`
+ lib/spack/spack/cmd/style.py:697:27: warning[possibly-missing-attribute] Attribute `lineno` may be missing on object of type `Str | (AST & ~Str)`
+ lib/spack/spack/cmd/style.py:697:40: warning[possibly-missing-attribute] Attribute `col_offset` may be missing on object of type `Str | (AST & ~Str)`
- lib/spack/spack/detection/path.py:169:33: error[invalid-argument-type] Argument to function `dedupe_paths` is incorrect: Expected `list[str]`, found `Unknown | list[str | bytes | Unknown | ... omitted 3 union elements]`
+ lib/spack/spack/detection/path.py:169:33: error[invalid-argument-type] Argument to function `dedupe_paths` is incorrect: Expected `list[str]`, found `Unknown | list[int | str | bytes | ... omitted 3 union elements]`
- lib/spack/spack/vendor/jinja2/exceptions.py:43:23: error[invalid-assignment] Object of type `str | Undefined | None` is not assignable to `str | None`
- lib/spack/spack/vendor/jinja2/parser.py:433:9: error[invalid-assignment] Object of type `Expr` is not assignable to attribute `call` of type `Call`
- lib/spack/spack/verify_libraries.py:164:46: error[invalid-argument-type] Argument to function `candidate_matches` is incorrect: Expected `bytes`, found `Unknown | bytes | str | PathLike[str] | PathLike[bytes]`
+ lib/spack/spack/verify_libraries.py:164:46: error[invalid-argument-type] Argument to function `candidate_matches` is incorrect: Expected `bytes`, found `bytes | Unknown | str | PathLike[str] | PathLike[bytes]`
- lib/spack/spack/verify_libraries.py:165:17: error[invalid-assignment] Invalid subscript assignment with key of type `Unknown | bytes | str | PathLike[str] | PathLike[bytes]` and value of type `Unknown | bytes | str | PathLike[str] | PathLike[bytes]` on object of type `dict[bytes, bytes]`
+ lib/spack/spack/verify_libraries.py:165:17: error[invalid-assignment] Invalid subscript assignment with key of type `bytes | Unknown | str | PathLike[str] | PathLike[bytes]` and value of type `bytes | Unknown | str | PathLike[str] | PathLike[bytes]` on object of type `dict[bytes, bytes]`
- lib/spack/spack/verify_libraries.py:170:57: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `list[bytes]`, found `list[Unknown | bytes | str | PathLike[str] | PathLike[bytes]]`
+ lib/spack/spack/verify_libraries.py:170:57: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `list[bytes]`, found `list[bytes | Unknown | str | PathLike[str] | PathLike[bytes]]`
- lib/spack/spack/verify_libraries.py:170:69: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `list[bytes]`, found `list[Unknown | bytes | str | PathLike[str] | PathLike[bytes]]`
+ lib/spack/spack/verify_libraries.py:170:69: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `list[bytes]`, found `list[bytes | Unknown | str | PathLike[str] | PathLike[bytes]]`
- Found 4398 diagnostics
+ Found 4382 diagnostics

pip (https://github.com/pypa/pip)
- src/pip/_internal/req/req_uninstall.py:132:42: error[invalid-argument-type] Argument to function `norm_join` is incorrect: Expected `str`, found `Sized | Unknown`
+ src/pip/_internal/req/req_uninstall.py:132:42: error[invalid-argument-type] Argument to function `norm_join` is incorrect: Expected `str`, found `Unknown | Sized`
- src/pip/_internal/req/req_uninstall.py:133:40: error[invalid-argument-type] Argument to function `norm_join` is incorrect: Expected `str`, found `Sized | Unknown`
+ src/pip/_internal/req/req_uninstall.py:133:40: error[invalid-argument-type] Argument to function `norm_join` is incorrect: Expected `str`, found `Unknown | Sized`
- src/pip/_internal/req/req_uninstall.py:139:27: error[unsupported-operator] Operator `+` is not supported between objects of type `Sized | Unknown` and `LiteralString`
+ src/pip/_internal/req/req_uninstall.py:139:27: error[unsupported-operator] Operator `+` is not supported between objects of type `Unknown | Sized` and `LiteralString`

beartype (https://github.com/beartype/beartype)
- beartype/_util/hint/pep/utilpepsign.py:239:12: error[invalid-return-type] Return type does not match returned value: expected `HintSign`, found `HintSign | None`
- Found 519 diagnostics
+ Found 518 diagnostics

asynq (https://github.com/quora/asynq)
- asynq/async_task.py:217:24: warning[possibly-missing-attribute] Attribute `send` may be missing on object of type `Unknown | None`
- asynq/async_task.py:219:47: error[invalid-argument-type] Argument to function `get_frame` is incorrect: Expected `GeneratorType[Unknown, None, None]`, found `Unknown | None`
- asynq/async_task.py:221:28: warning[possibly-missing-attribute] Attribute `throw` may be missing on object of type `Unknown | None`
- asynq/async_task.py:223:28: warning[possibly-missing-attribute] Attribute `throw` may be missing on object of type `Unknown | None`
- Found 194 diagnostics
+ Found 190 diagnostics

yarl (https://github.com/aio-libs/yarl)
- yarl/_url.py:1663:20: error[no-matching-overload] No overload of function `lru_cache` matches arguments
- Found 45 diagnostics
+ Found 44 diagnostics

jinja (https://github.com/pallets/jinja)
- src/jinja2/exceptions.py:43:23: error[invalid-assignment] Object of type `str | Undefined | None` is not assignable to `str | None`
- src/jinja2/parser.py:429:9: error[invalid-assignment] Object of type `Expr` is not assignable to attribute `call` of type `Call`
- Found 181 diagnostics
+ Found 179 diagnostics

kopf (https://github.com/nolar/kopf)
- kopf/_cogs/structs/credentials.py:289:16: error[unsupported-operator] Operator `not in` is not supported between objects of type `str` and `dict[str, object] | None`
- kopf/_cogs/structs/credentials.py:291:24: error[unsupported-operator] Operator `not in` is not supported between objects of type `str` and `dict[str, object] | None`
- kopf/_cogs/structs/credentials.py:292:25: error[invalid-assignment] Cannot assign to a subscript on an object of type `None`
- kopf/_cogs/structs/credentials.py:293:44: error[not-subscriptable] Cannot subscript object of type `None` with no `__getitem__` method
- Found 294 diagnostics
+ Found 290 diagnostics

pytest (https://github.com/pytest-dev/pytest)
- src/_pytest/python.py:951:31: error[not-subscriptable] Cannot subscript object of type `_HiddenParam` with no `__getitem__` method
- src/_pytest/python.py:953:45: error[invalid-argument-type] Method `__getitem__` of type `bound method defaultdict[str, int].__getitem__(key: str, /) -> int` cannot be called with key of type `_HiddenParam` on object of type `defaultdict[str, int]`
- src/_pytest/python.py:955:25: error[invalid-argument-type] Method `__getitem__` of type `bound method defaultdict[str, int].__getitem__(key: str, /) -> int` cannot be called with key of type `_HiddenParam` on object of type `defaultdict[str, int]`
- src/_pytest/python.py:956:49: error[invalid-argument-type] Method `__getitem__` of type `bound method defaultdict[str, int].__getitem__(key: str, /) -> int` cannot be called with key of type `_HiddenParam` on object of type `defaultdict[str, int]`
- src/_pytest/python.py:958:21: error[invalid-argument-type] Method `__getitem__` of type `bound method defaultdict[str, int].__getitem__(key: str, /) -> int` cannot be called with key of type `_HiddenParam` on object of type `defaultdict[str, int]`
- src/_pytest/raises.py:1427:56: error[invalid-argument-type] Argument to function `issubclass` is incorrect: Expected `type`, found `type[BaseException] | None`
- src/_pytest/terminal.py:1378:16: error[invalid-return-type] Return type does not match returned value: expected `tuple[str, list[str]]`, found `tuple[str | None, list[str] | None]`
+ src/_pytest/unittest.py:624:28: warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- Found 396 diagnostics
+ Found 390 diagnostics

paasta (https://github.com/yelp/paasta)
- paasta_tools/cleanup_expired_autoscaling_overrides.py:72:12: warning[possibly-missing-attribute] Attribute `data` may be missing on object of type `Unknown | None`
- paasta_tools/cleanup_expired_autoscaling_overrides.py:83:44: warning[possibly-missing-attribute] Attribute `data` may be missing on object of type `Unknown | None`
- paasta_tools/cleanup_expired_autoscaling_overrides.py:136:9: error[invalid-assignment] Object of type `dict[Unknown, Unknown]` is not assignable to attribute `data` on type `Unknown | None`
- paasta_tools/cli/cmds/mesh_status.py:111:23: warning[possibly-missing-attribute] Attribute `service` may be missing on object of type `PaastaOApiClient | None`
- paasta_tools/cli/cmds/mesh_status.py:115:12: warning[possibly-missing-attribute] Attribute `api_error` may be missing on object of type `PaastaOApiClient | None`
- paasta_tools/cli/cmds/mesh_status.py:122:13: warning[possibly-missing-attribute] Attribute `connection_error` may be missing on object of type `PaastaOApiClient | None`
- paasta_tools/cli/cmds/mesh_status.py:122:38: warning[possibly-missing-attribute] Attribute `timeout_error` may be missing on object of type `PaastaOApiClient | None`
- paasta_tools/cli/cmds/start_stop_restart.py:313:17: warning[possibly-missing-attribute] Attribute `service` may be missing on object of type `PaastaOApiClient | None`
- paasta_tools/cli/cmds/start_stop_restart.py:318:20: warning[possibly-missing-attribute] Attribute `api_error` may be missing on object of type `PaastaOApiClient | None`
- paasta_tools/cli/cmds/status.py:322:18: warning[possibly-missing-attribute] Attribute `service` may be missing on object of type `PaastaOApiClient | None`
- paasta_tools/cli/cmds/status.py:329:12: warning[possibly-missing-attribute] Attribute `api_error` may be missing on object of type `PaastaOApiClient | None`
- paasta_tools/cli/cmds/status.py:332:13: warning[possibly-missing-attribute] Attribute `connection_error` may be missing on object of type `PaastaOApiClient | None`
- paasta_tools/cli/cmds/status.py:332:38: warning[possibly-missing-attribute] Attribute `timeout_error` may be missing on object of type `PaastaOApiClient | None`
- paasta_tools/kubernetes_tools.py:4454:12: error[invalid-return-type] Return type does not match returned value: expected `int | None`, found `str | int | None`
- paasta_tools/oom_logger.py:269:5: warning[possibly-missing-attribute] Attribute `config` may be missing on object of type `Unknown | None`
- Found 1076 diagnostics
+ Found 1061 diagnostics

scrapy (https://github.com/scrapy/scrapy)
- scrapy/cmdline.py:186:11: error[invalid-argument-type] Method `__getitem__` of type `bound method dict[str, ScrapyCommand].__getitem__(key: str, /) -> ScrapyCommand` cannot be called with key of type `None` on object of type `dict[str, ScrapyCommand]`
- Found 1776 diagnostics
+ Found 1775 diagnostics

pylint (https://github.com/pycqa/pylint)
- pylint/checkers/async_checker.py:92:63: warning[possibly-missing-attribute] Attribute `name` may be missing on object of type `None | Unknown`
+ pylint/checkers/async_checker.py:92:63: warning[possibly-missing-attribute] Attribute `name` may be missing on object of type `(Unknown & ~AlwaysTruthy) | (Unknown & ~AlwaysFalsy) | None`
- pylint/checkers/refactoring/implicit_booleaness_checker.py:219:24: warning[possibly-missing-attribute] Attribute `as_string` may be missing on object of type `str | Unknown`
+ pylint/checkers/refactoring/implicit_booleaness_checker.py:219:24: warning[possibly-missing-attribute] Attribute `as_string` may be missing on object of type `Unknown | str`
- pylint/checkers/refactoring/implicit_booleaness_checker.py:219:62: warning[possibly-missing-attribute] Attribute `as_string` may be missing on object of type `str | Unknown`
+ pylint/checkers/refactoring/implicit_booleaness_checker.py:219:62: warning[possibly-missing-attribute] Attribute `as_string` may be missing on object of type `Unknown | str`
- pylint/checkers/refactoring/implicit_booleaness_checker.py:222:27: warning[possibly-missing-attribute] Attribute `as_string` may be missing on object of type `str | (Unknown & ~None)`
+ pylint/checkers/refactoring/implicit_booleaness_checker.py:222:27: warning[possibly-missing-attribute] Attribute `as_string` may be missing on object of type `(Unknown & ~None) | str`
- pylint/checkers/refactoring/implicit_booleaness_checker.py:236:29: warning[possibly-missing-attribute] Attribute `as_string` may be missing on object of type `str | Unknown`
+ pylint/checkers/refactoring/implicit_booleaness_checker.py:236:29: warning[possibly-missing-attribute] Attribute `as_string` may be missing on object of type `Unknown | str`
- pylint/checkers/refactoring/implicit_booleaness_checker.py:239:29: warning[possibly-missing-attribute] Attribute `as_string` may be missing on object of type `str | Unknown`
+ pylint/checkers/refactoring/implicit_booleaness_checker.py:239:29: warning[possibly-missing-attribute] Attribute `as_string` may be missing on object of type `Unknown | str`
- pylint/checkers/typecheck.py:729:46: warning[possibly-missing-attribute] Attribute `parent` may be missing on object of type `None | Unknown`
- pylint/checkers/typecheck.py:732:34: warning[possibly-missing-attribute] Attribute `parent` may be missing on object of type `None | Unknown`
- pylint/checkers/typecheck.py:734:34: warning[possibly-missing-attribute] Attribute `statement` may be missing on object of type `None | Unknown`
- Found 223 diagnostics
+ Found 220 diagnostics

rich (https://github.com/Textualize/rich)
- tests/test_tools.py:17:17: error[invalid-argument-type] Argument to function `next` is incorrect: Expected `SupportsNext[Unknown]`, found `Iterable[tuple[bool, str | Unknown]]`
+ tests/test_tools.py:17:17: error[invalid-argument-type] Argument to function `next` is incorrect: Expected `SupportsNext[Unknown]`, found `Iterable[tuple[bool, Unknown | str]]`
- tests/test_tools.py:18:17: error[invalid-argument-type] Argument to function `next` is incorrect: Expected `SupportsNext[Unknown]`, found `Iterable[tuple[bool, str | Unknown]]`
+ tests/test_tools.py:18:17: error[invalid-argument-type] Argument to function `next` is incorrect: Expected `SupportsNext[Unknown]`, found `Iterable[tuple[bool, Unknown | str]]`
- tests/test_tools.py:19:17: error[invalid-argument-type] Argument to function `next` is incorrect: Expected `SupportsNext[Unknown]`, found `Iterable[tuple[bool, str | Unknown]]`
+ tests/test_tools.py:19:17: error[invalid-argument-type] Argument to function `next` is incorrect: Expected `SupportsNext[Unknown]`, found `Iterable[tuple[bool, Unknown | str]]`
- tests/test_tools.py:20:17: error[invalid-argument-type] Argument to function `next` is incorrect: Expected `SupportsNext[Unknown]`, found `Iterable[tuple[bool, str | Unknown]]`
+ tests/test_tools.py:20:17: error[invalid-argument-type] Argument to function `next` is incorrect: Expected `SupportsNext[Unknown]`, found `Iterable[tuple[bool, Unknown | str]]`

dulwich (https://github.com/dulwich/dulwich)
- dulwich/notes.py:644:20: error[invalid-argument-type] Argument is incorrect: Expected `bytes`, found `bytes | None`
- Found 223 diagnostics
+ Found 222 diagnostics

mkosi (https://github.com/systemd/mkosi)
- mkosi/__init__.py:1700:9: error[invalid-argument-type] Argument to function `run` is incorrect: Expected `Sequence[Path | str]`, found `list[Unknown | Path | str | None]`
- mkosi/__init__.py:1793:17: error[invalid-argument-type] Argument to function `systemd_tool_version` is incorrect: Expected `Path | str`, found `Unknown | Path | None`
- mkosi/__init__.py:1853:17: error[invalid-argument-type] Argument to function `systemd_tool_version` is incorrect: Expected `Path | str`, found `Unknown | Path | None`
- mkosi/__init__.py:1910:27: warning[possibly-missing-attribute] Attribute `group` may be missing on object of type `Match[str] | None`
- mkosi/__init__.py:2601:17: error[invalid-argument-type] Argument to bound method `sandbox` is incorrect: Expected `Sequence[Path | str]`, found `list[str | Unknown | None | Path]`
- mkosi/__init__.py:2809:12: error[invalid-return-type] Return type does not match returned value: expected `Path`, found `Path | None`
- mkosi/__init__.py:4470:9: error[invalid-argument-type] Argument to function `run` is incorrect: Expected `Sequence[Path | str]`, found `list[Path | None | str]`
- mkosi/__init__.py:4561:15: error[unsupported-operator] Operator `/` is not supported between objects of type `Path | None` and `Literal["mkosi.key"]`
- mkosi/__init__.py:4561:40: error[unsupported-operator] Operator `/` is not supported between objects of type `Path | None` and `Literal["mkosi.crt"]`
- mkosi/__init__.py:4585:24: error[unsupported-operator] Operator `/` is not supported between objects of type `Path | None` and `Literal["mkosi.key"]`
- mkosi/__init__.py:4586:21: error[unsupported-operator] Operator `/` is not supported between objects of type `Path | None` and `Literal["mkosi.crt"]`
- mkosi/__init__.py:4599:9: error[unsupported-operator] Operator `/` is not supported between objects of type `Path | None` and `Literal["mkosi.version"]`
- mkosi/bootloader.py:395:52: error[invalid-argument-type] Argument to function `extract_pe_section` is incorrect: Expected `Path`, found `Path | None`
- mkosi/config.py:291:16: error[invalid-return-type] Return type does not match returned value: expected `str`, found `Unknown | str | None`
- mkosi/config.py:497:16: error[invalid-return-type] Return type does not match returned value: expected `Architecture`, found `Unknown | Architecture | None`
- mkosi/config.py:540:16: error[invalid-return-type] Return type does not match returned value: expected `str`, found `Unknown | str | None`
- mkosi/config.py:562:16: error[invalid-return-type] Return type does not match returned value: expected `str`, found `Unknown | str | None`
- mkosi/config.py:707:12: error[invalid-return-type] Return type does not match returned value: expected `bool`, found `bool | None`
- mkosi/config.py:4934:30: error[call-non-callable] Object of type `None` is not callable
- mkosi/distribution/arch.py:148:16: error[invalid-return-type] Return type does not match returned value: expected `str`, found `Unknown | str | None`
- mkosi/distribution/azure.py:112:16: error[invalid-return-type] Return type does not match returned value: expected `str`, found `Unknown | str | None`
- mkosi/distribution/centos.py:96:16: error[invalid-return-type] Return type does not match returned value: expected `str`, found `Unknown | str | None`
- mkosi/distribution/debian.py:257:16: error[invalid-return-type] Return type does not match returned value: expected `str`, found `Unknown | str | None`
- mkosi/distribution/fedora.py:57:23: warning[possibly-missing-attribute] Attribute `group` may be missing on object of type `Match[str] | None`
- mkosi/distribution/fedora.py:268:16: error[invalid-return-type] Return type does not match returned value: expected `str`, found `Unknown | str | None`
- mkosi/distribution/kali.py:65:16: error[invalid-return-type] Return type does not match returned value: expected `str`, found `Unknown | str | None`
- mkosi/distribution/mageia.py:67:16: error[invalid-return-type] Return type does not match returned value: expected `str`, found `Unknown | str | None`
- mkosi/distribution/openmandriva.py:63:16: error[invalid-return-type] Return type does not match returned value: expected `str`, found `Unknown | str | None`
- mkosi/distribution/opensuse.py:280:16: error[invalid-return-type] Return type does not match returned value: expected `str`, found `Unknown | str | None`
- mkosi/distribution/opensuse.py:340:18: warning[possibly-missing-attribute] Attribute `iter` may be missing on object of type `Element[str] | None`
- mkosi/distribution/postmarketos.py:111:16: error[invalid-return-type] Return type does not match returned value: expected `str`, found `Unknown | str | None`
- mkosi/qemu.py:341:33: error[invalid-assignment] Object of type `list[Path | str | None]` is not assignable to `list[Path | str]`
- mkosi/qemu.py:496:13: error[invalid-argument-type] Argument is incorrect: Expected `Sequence[Path | str]`, found `list[Path | None | str]`
- mkosi/sysupdate.py:70:33: error[invalid-assignment] Object of type `list[Path | str | None]` is not assignable to `list[Path | str]`
- Found 75 diagnostics
+ Found 41 diagnostics

nox (https://github.com/wntrblm/nox)
- nox/sessions.py:780:49: warning[possibly-missing-attribute] Attribute `_reused` may be missing on object of type `(Unknown & ~PassthroughEnv) | (ProcessEnv & ~PassthroughEnv) | None`
- nox/sessions.py:792:29: warning[possibly-missing-attribute] Attribute `is_offline` may be missing on object of type `Unknown | ProcessEnv | None`
- nox/sessions.py:806:13: warning[possibly-missing-attribute] Attribute `conda_cmd` may be missing on object of type `Unknown | ProcessEnv | None`
- Found 24 diagnostics
+ Found 21 diagnostics

poetry (https://github.com/python-poetry/poetry)
- src/poetry/inspection/info.py:466:9: error[invalid-assignment] Object of type `Literal["directory"]` is not assignable to attribute `_source_type` on type `PackageInfo | None`
- src/poetry/inspection/info.py:467:9: error[invalid-assignment] Object of type `str` is not assignable to attribute `_source_url` on type `PackageInfo | None`
- src/poetry/inspection/info.py:469:16: error[invalid-return-type] Return type does not match returned value: expected `PackageInfo`, found `PackageInfo | None`
- Found 979 diagnostics
+ Found 976 diagnostics

schemathesis (https://github.com/schemathesis/schemathesis)
- src/schemathesis/specs/openapi/schemas.py:322:32: warning[possibly-missing-attribute] Attribute `items` may be missing on object of type `Mapping[str, Any] | None`
- Found 322 diagnostics
+ Found 321 diagnostics

urllib3 (https://github.com/urllib3/urllib3)
- src/urllib3/contrib/emscripten/fetch.py:398:32: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `Unknown | None`
- src/urllib3/contrib/emscripten/fetch.py:404:43: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `Unknown | None`
- test/test_response.py:669:31: error[not-iterable] Object of type `None` is not iterable
- test/test_response.py:706:31: error[not-iterable] Object of type `None` is not iterable
- test/test_response.py:761:31: error[not-iterable] Object of type `None` is not iterable
- test/test_response.py:816:33: error[not-iterable] Object of type `None` is not iterable
- Found 285 diagnostics
+ Found 279 diagnostics

pydantic (https://github.com/pydantic/pydantic)
- pydantic/v1/utils.py:613:16: error[invalid-return-type] Return type does not match returned value: expected `Mapping[int | str, Any]`, found `AbstractSet[int | str] | Mapping[int | str, Any] | dict[int | str, ellipsis]`
+ pydantic/v1/utils.py:613:16: error[invalid-return-type] Return type does not match returned value: expected `Mapping[int | str, Any]`, found `(AbstractSet[int | str] & Top[Mapping[Unknown, object]]) | (Mapping[int | str, Any] & AbstractSet[object]) | (Mapping[int | str, Any] & ~AbstractSet[object]) | dict[int | str, ellipsis]`

artigraph (https://github.com/artigraph/artigraph)
- tests/arti/types/test_types.py:100:51: error[invalid-argument-type] Argument is incorrect: Expected `frozenset[Any]`, found `frozenset[float | Unknown | int] | list[Unknown | int | float] | tuple[float | Unknown | int, ...]`
+ tests/arti/types/test_types.py:100:51: error[invalid-argument-type] Argument is incorrect: Expected `frozenset[Any]`, found `frozenset[float | Unknown | int] | list[int | Unknown | float] | tuple[float | Unknown | int, ...]`

psycopg (https://github.com/psycopg/psycopg)
- tests/utils.py:95:17: warning[possibly-missing-attribute] Attribute `group` may be missing on object of type `Match[str] | None`
- tests/utils.py:96:14: warning[possibly-missing-attribute] Attribute `group` may be missing on object of type `Match[str] | None`
- tests/utils.py:97:47: warning[possibly-missing-attribute] Attribute `groups` may be missing on object of type `Match[str] | None`
- Found 654 diagnostics
+ Found 651 diagnostics

vision (https://github.com/pytorch/vision)
- release/apply-release-changes.py:89:20: error[unsupported-operator] Operator `/` is not supported between objects of type `Path | None` and `Literal[".github"]`
- Found 1409 diagnostics
+ Found 1408 diagnostics

mypy (https://github.com/python/mypy)
- mypy/semanal_newtype.py:125:82: error[invalid-argument-type] Argument to function `has_any_from_unimported_type` is incorrect: Expected `Type`, found `ProperType | None`
- mypy/semanal_newtype.py:126:80: error[invalid-argument-type] Argument to bound method `unimported_type_becomes_any` is incorrect: Expected `Type`, found `ProperType | None`
- mypy/test/data.py:146:23: warning[possibly-missing-attribute] Attribute `group` may be missing on object of type `Match[str] | None`
- mypy/test/data.py:149:36: warning[possibly-missing-attribute] Attribute `group` may be missing on object of type `Match[str] | None`
- Found 1723 diagnostics
+ Found 1719 diagnostics

Tanjun (https://github.com/FasterSpeeding/Tanjun)
- tanjun/annotations.py:1469:46: error[invalid-type-form] Variable of type `type[Unknown]` is not allowed in a type expression
- Found 130 diagnostics
+ Found 129 diagnostics

mongo-python-driver (https://github.com/mongodb/mongo-python-driver)
- gridfs/asynchronous/grid_file.py:1873:26: warning[possibly-missing-attribute] Attribute `next` may be missing on object of type `AsyncCursor[Any] | None`
- gridfs/asynchronous/grid_file.py:1875:19: warning[possibly-missing-attribute] Attribute `close` may be missing on object of type `AsyncCursor[Any] | None`
- gridfs/asynchronous/grid_file.py:1877:26: warning[possibly-missing-attribute] Attribute `next` may be missing on object of type `AsyncCursor[Any] | None`
- gridfs/synchronous/grid_file.py:1861:20: warning[possibly-missing-attribute] Attribute `next` may be missing on object of type `Cursor[Any] | None`
- gridfs/synchronous/grid_file.py:1863:13: warning[possibly-missing-attribute] Attribute `close` may be missing on object of type `Cursor[Any] | None`
- gridfs/synchronous/grid_file.py:1865:20: warning[possibly-missing-attribute] Attribute `next` may be missing on object of type `Cursor[Any] | None`
- Found 440 diagnostics
+ Found 434 diagnostics

discord.py (https://github.com/Rapptz/discord.py)
- discord/audit_logs.py:550:56: error[invalid-argument-type] Argument to bound method `from_data` is incorrect: Expected `int`, found `None | Unknown | int`
- discord/audit_logs.py:551:55: error[invalid-argument-type] Argument to bound method `from_data` is incorrect: Expected `int`, found `None | Unknown | int`
- Found 534 diagnostics
+ Found 532 diagnostics

trio (https://github.com/python-trio/trio)
- src/trio/testing/_fake_net.py:324:56: error[invalid-argument-type] Argument to bound method `from_python_sockaddr` is incorrect: Expected `tuple[str, int] | tuple[str, int, int, int]`, found `None | tuple[str, int]`
- Found 471 diagnostics
+ Found 470 diagnostics

pyodide (https://github.com/pyodide/pyodide)
- pyodide-build/pyodide_build/pywasmcross.py:384:24: error[no-matching-overload] No overload of function `run` matches arguments
- pyodide-build/pyodide_build/pywasmcross.py:392:27: error[no-matching-overload] No overload of bound method `join` matches arguments
- Found 938 diagnostics
+ Found 936 diagnostics

cibuildwheel (https://github.com/pypa/cibuildwheel)
- cibuildwheel/resources/_rust_shim.py:53:14: error[invalid-argument-type] Argument to function `execv` is incorrect: Expected `str | bytes | PathLike[str] | PathLike[bytes]`, found `str | None`
- cibuildwheel/resources/_rust_shim.py:53:24: error[invalid-argument-type] Argument to function `execv` is incorrect: Expected `tuple[str | bytes | PathLike[str] | PathLike[bytes], ...] | list[bytes] | list[str] | ... omitted 5 union elements`, found `list[Unknown | str | None]`
- Found 29 diagnostics
+ Found 27 diagnostics

meson (https://github.com/mesonbuild/meson)
- mesonbuild/backend/vs2010backend.py:378:27: warning[possibly-missing-attribute] Attribute `get_generated_sources` may be missing on object of type `(Target & ~CustomTargetIndex) | CustomTarget | CompileTarget`
- mesonbuild/dependencies/cuda.py:137:76: error[invalid-argument-type] Argument to function `version_compare_many` is incorrect: Expected `str`, found `str | None | Unknown`
+ mesonbuild/dependencies/cuda.py:137:76: error[invalid-argument-type] Argument to function `version_compare_many` is incorrect: Expected `str`, found `Unknown | str | None`
- mesonbuild/interpreter/interpreter.py:815:59: error[unresolved-attribute] Object of type `Executable | Program | Compiler | File | str` has no attribute `get_name`
+ mesonbuild/interpreter/interpreter.py:815:59: warning[possibly-missing-attribute] Attribute `get_name` may be missing on object of type `(Program & ~Executable) | ExternalProgram`
- mesonbuild/interpreter/interpreter.py:830:33: warning[possibly-missing-attribute] Attribute `get_path` may be missing on object of type `Executable | Program | Compiler | File | str`
- mesonbuild/interpreter/interpreter.py:835:27: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `Program`, found `Executable | Program | Compiler | File | str`
- mesonbuild/interpreter/interpreter.py:2304:22: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `ExternalProgram | Executable | CustomTarget | CustomTargetIndex`, found `Executable | Jar | Program | ... omitted 4 union elements`
+ mesonbuild/interpreter/interpreter.py:2304:22: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `ExternalProgram | Executable | CustomTarget | CustomTargetIndex`, found `Program | Executable | (Jar & ~LocalProgram & ~File) | ... omitted 3 union elements`
- mesonbuild/interpreter/mesonmain.py:77:22: error[invalid-argument-type] Argument to bound method `append` is incorrect: Expected `str | Executable | Program`, found `str | File | Executable | Program | Unknown`
- mesonbuild/modules/rust.py:601:13: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `Sequence[str | File | BuildTarget | ... omitted 5 union elements]`, found `list[Unknown | File | CustomTarget | ... omitted 5 union elements]`
- mesonbuild/options.py:889:16: error[invalid-return-type] Return type does not match returned value: expected `UserBooleanOption | UserComboOption | UserIntegerOption | ... omitted 3 union elements`, found `UserBooleanOption | UserComboOption | UserIntegerOption | ... omitted 4 union elements`
- packaging/createmsi.py:131:31: error[invalid-argument-type] Argument to function `check_call` is incorrect: Expected `Sequence[str | bytes | PathLike[str] | PathLike[bytes]] | bytes | PathLike[str] | PathLike[bytes]`, found `list[Unknown | str | None]`
- packaging/createmsi.py:194:44: error[invalid-argument-type] Argument to function `SubElement` is incorrect: Expected `dict[str, str]`, found `dict[str, str | Unknown | None]`
- Found 2214 diagnostics
+ Found 2206 diagnostics

openlibrary (https://github.com/internetarchive/openlibrary)
- openlibrary/core/lists/model.py:474:27: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `Thing | str | AnnotatedSeed`, found `str | ThingReferenceDict | AnnotatedSeedDict`
+ openlibrary/core/lists/model.py:474:27: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `Thing | str | AnnotatedSeed`, found `str | (ThingReferenceDict & ~Top[dict[Unknown, Unknown]]) | (AnnotatedSeedDict & ~Top[dict[Unknown, Unknown]])`

cloud-init (https://github.com/canonical/cloud-init)
- cloudinit/config/cc_growpart.py:214:12: error[invalid-return-type] Return type does not match returned value: expected `Resizer`, found `None | (ResizeGrowPart & ~AlwaysFalsy) | (ResizeGrowFS & ~AlwaysFalsy) | (ResizeGpart & ~AlwaysFalsy) | (Unknown & ~AlwaysFalsy)`
- cloudinit/config/cc_mounts.py:189:25: error[invalid-argument-type] Argument to function `min` is incorrect: Argument type `Unknown | None | int` does not satisfy upper bound `SupportsDunderLT[Any] | SupportsDunderGT[Any]` of type variable `SupportsRichComparisonT`
- cloudinit/util.py:2007:26: error[not-iterable] Object of type `list[Unknown] | None | list[Unknown | str]` may not be iterable
- tests/unittests/distros/test_user_data_normalize.py:24:31: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `dict[Unknown, Unknown]`, found `Unknown | dict[Unknown | str, Unknown | str] | str | ... omitted 3 union elements`
+ tests/unittests/distros/test_user_data_normalize.py:24:31: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `dict[Unknown, Unknown]`, found `Unknown | bool | list[Unknown] | ... omitted 3 union elements`
- tests/unittests/sources/test_gce.py:71:31: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `dict[Unknown, Unknown]`, found `Unknown | dict[Unknown | str, Unknown | str] | str | ... omitted 3 union elements`
+ tests/unittests/sources/test_gce.py:71:31: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `dict[Unknown, Unknown]`, found `Unknown | bool | list[Unknown] | ... omitted 3 union elements`
- tests/unittests/sources/test_ibmcloud.py:278:56: error[not-subscriptable] Cannot subscript object of type `None` with no `__getitem__` method
- tests/unittests/sources/test_ibmcloud.py:279:26: error[not-subscriptable] Cannot subscript object of type `None` with no `__getitem__` method
- tests/unittests/sources/test_ibmcloud.py:280:33: error[not-subscriptable] Cannot subscript object of type `None` with no `__getitem__` method
- tests/unittests/sources/test_ibmcloud.py:281:65: error[not-subscriptable] Cannot subscript object of type `None` with no `__getitem__` method
- tests/unittests/sources/test_ibmcloud.py:282:32: error[not-subscriptable] Cannot subscript object of type `None` with no `__getitem__` method
- tests/unittests/sources/test_ibmcloud.py:304:41: error[not-subscriptable] Cannot subscript object of type `None` with no `__getitem__` method
- tests/unittests/sources/test_ibmcloud.py:305:26: error[not-subscriptable] Cannot subscript object of type `None` with no `__getitem__` method
- tests/unittests/sources/test_ibmcloud.py:306:33: error[not-subscriptable] Cannot subscript object of type `None` with no `__getitem__` method
- tests/unittests/sources/test_ibmcloud.py:307:63: error[not-subscriptable] Cannot subscript object of type `None` with no `__getitem__` method
- tests/unittests/sources/test_ibmcloud.py:326:41: error[not-subscriptable] Cannot subscript object of type `None` with no `__getitem__` method
- tests/unittests/sources/test_ibmcloud.py:327:26: error[not-subscriptable] Cannot subscript object of type `None` with no `__getitem__` method
- tests/unittests/sources/test_ibmcloud.py:328:16: error[not-subscriptable] Cannot subscript object of type `None` with no `__getitem__` method
- tests/unittests/sources/test_ibmcloud.py:329:63: error[not-subscriptable] Cannot subscript object of type `None` with no `__getitem__` method
- Found 1166 diagnostics
+ Found 1150 diagnostics

setuptools (https://github.com/pypa/setuptools)
- setuptools/_distutils/dist.py:74:12: error[invalid-return-type] Return type does not match returned value: expected `str | list[str]`, found `Iterable[str] | list[Unknown]`
+ setuptools/_distutils/dist.py:74:12: error[invalid-return-type] Return type does not match returned value: expected `str | list[str]`, found `str | (Iterable[str] & Top[list[Unknown]]) | list[Unknown]`

cwltool (https://github.com/common-workflow-language/cwltool)
- cwltool/main.py:391:12: error[invalid-return-type] Return type does not match returned value: expected `tuple[MutableMapping[str, None | int | str | ... omitted 3 union elements] | None, str, Loader]`, found `tuple[None | int | float | ... omitted 3 union elements, Unknown | str, Loader]`
+ cwltool/main.py:391:12: error[invalid-return-type] Return type does not match returned value: expected `tuple[MutableMapping[str, None | int | str | ... omitted 3 union elements] | None, str, Loader]`, found `tuple[None | (int & Top[MutableMapping[Unknown, Unknown]]) | (float & Top[MutableMapping[Unknown, Unknown]]) | ... omitted 3 union elements, Unknown | str, Loader]`
- cwltool/main.py:477:33: warning[possibly-missing-attribute] Attribute `get` may be missing on object of type `MutableMapping[str, None | int | str | ... omitted 3 union elements] | None`
- cwltool/main.py:499:37: error[invalid-argument-type] Argument to bound method `_init_job` is incorrect: Expected `MutableMapping[str, None | int | str | ... omitted 3 union elements]`, found `MutableMapping[str, None | int | str | ... omitted 3 union elements] | None`
- cwltool/main.py:502:43: error[invalid-argument-type] Argument to bound method `bind_input` is incorrect: Expected `MutableMapping[str, None | int | str | ... omitted 3 union elements] | list[MutableMapping[str, None | int | str | ... omitted 3 union elements]]`, found `MutableMapping[str, None | int | str | ... omitted 3 union elements] | None`
- cwltool/main.py:505:25: error[not-subscriptable] Cannot subscript object of type `None` with no `__getitem__` method
- cwltool/main.py:510:13: error[invalid-argument-type] Argument to function `printdeps` is incorrect: Expected `MutableMapping[str, None | int | str | ... omitted 3 union elements]`, found `MutableMapping[str, None | int | str | ... omitted 3 union elements] | None`
- cwltool/main.py:523:13: error[invalid-argument-type] Argument to bound method `store` is incorrect: Expected `MutableMapping[str, None | int | str | ... omitted 3 union elements]`, found `MutableMapping[str, None | int | str | ... omitted 3 union elements] | None`
- cwltool/main.py:526:8: error[unsupported-operator] Operator `in` is not supported between objects of type `Literal["cwl:tool"]` and `MutableMapping[str, None | int | str | ... omitted 3 union elements] | None`
- cwltool/main.py:527:13: error[not-subscriptable] Cannot delete subscript on object of type `None` with no `__delitem__` method
- cwltool/main.py:528:8: error[unsupported-operator] Operator `in` is not supported between objects of type `Literal["__id"]` and `MutableMapping[str, None | int | str | ... omitted 3 union elements] | None`
- cwltool/main.py:529:13: error[not-subscriptable] Cannot delete subscript on object of type `None` with no `__delitem__` method
- cwltool/main.py:530:12: error[invalid-return-type] Return type does not match returned value: expected `MutableMapping[str, None | int | str | ... omitted 3 union elements]`, found `MutableMapping[str, None | int | str | ... omitted 3 union elements] | None`
- Found 510 diagnostics
+ Found 499 diagnostics

archinstall (https://github.com/archlinux/archinstall)
- archinstall/lib/models/mirrors.py:85:10: error[invalid-return-type] Return type does not match returned value: expected `int | float`, found `int | float | None`
- archinstall/tui/ui/components.py:1203:10: error[invalid-return-type] Return type does not match returned value: expected `Result[ValueT@run]`, found `(Any & ~Exception) | None`
- Found 71 diagnostics
+ Found 69 diagnostics

manticore (https://github.com/trailofbits/manticore)
- manticore/native/state.py:129:15: error[no-matching-overload] No overload of bound method `get` matches arguments
- manticore/native/state.py:135:20: error[no-matching-overload] No overload of bound method `get` matches arguments
- manticore/native/state.py:136:17: error[invalid-argument-type] Method `__delitem__` of type `bound method dict[int | None, set[(StateBase, /) -> None]].__delitem__(key: int | None, /) -> None` cannot be called with key of type `int | str | None | (Any & ~str)` on object of type `dict[int | None, set[(StateBase, /) -> None]]`
- manticore/native/state.py:173:9: error[no-matching-overload] No overload of bound method `setdefault` matches arguments
- manticore/platforms/wasm.py:264:25: error[invalid-argument-type] Argument to bound method `append` is incorrect: Expected `ProtoFuncInst`, found `(ProtoFuncInst & ~FuncAddr & ~TableAddr & ~MemAddr & ~GlobalAddr & ~AlwaysFalsy) | (TableInst & ~FuncAddr & ~TableAddr & ~MemAddr & ~GlobalAddr & ~AlwaysFalsy) | (MemInst & ~FuncAddr & ~TableAddr & ~MemAddr & ~GlobalAddr & ~AlwaysFalsy) | ... omitted 3 union elements`
+ manticore/platforms/wasm.py:264:25: error[invalid-argument-type] Argument to bound method `append` is incorrect: Expected `ProtoFuncInst`, found `(ProtoFuncInst & Top[(...) -> object] & ~FuncAddr & ~TableAddr & ~MemAddr & ~GlobalAddr & ~AlwaysFalsy) | (TableInst & Top[(...) -> object] & ~FuncAddr & ~TableAddr & ~MemAddr & ~GlobalAddr & ~AlwaysFalsy) | (MemInst & Top[(...) -> object] & ~FuncAddr & ~TableAddr & ~MemAddr & ~GlobalAddr & ~AlwaysFalsy) | ... omitted 7 union elements`
- Found 11088 diagnostics
+ Found 11084 diagnostics

prefect (https://github.com/PrefectHQ/prefect)
- src/integrations/prefect-dbt/prefect_dbt/core/runner.py:196:16: error[invalid-return-type] Return type does not match returned value: expected `str`, found `str | None`
- src/prefect/_internal/analytics/service.py:62:16: error[invalid-return-type] Return type does not match returned value: expected `AnalyticsService`, found `AnalyticsService | None`
- src/prefect/cli/block.py:493:59: warning[possibly-missing-attribute] Attribute `fields` may be missing on object of type `BlockSchema | None`
- src/prefect/cli/block.py:495:43: warning[possibly-missing-attribute] Attribute `fields` may be missing on object of type `BlockSchema | None`
- src/prefect/cli/dashboard.py:27:62: error[invalid-argument-type] Argument to function `run_sync_in_worker_thread` is incorrect: Expected `str`, found `str | None`
- src/prefect/cli/deployment.py:1085:12: warning[possibly-missing-attribute] Attribute `is_completed` may be missing on object of type `State[Any] | None`
- src/prefect/cli/deployment.py:1087:54: warning[possibly-missing-attribute] Attribute `name` may be missing on object of type `State[Any] | None`
- src/prefect/cli/deployment.py:1090:43: warning[possibly-missing-attribute] Attribute `name` may be missing on object of type `State[Any] | None`
- src/prefect/cli/events.py:179:9: error[invalid-argument-type] Argument is incorrect: Expected `Resource`, found `dict[Unknown, Unknown] | Any`
+ src/prefect/cli/events.py:179:9: error[invalid-argument-type] Argument is incorrect: Expected `Resource`, found `dict[Unknown, Unknown] | (Any & Top[dict[Unknown, Unknown]])`
- src/prefect/cli/flow_run.py:153:66: error[invalid-argument-type] Argument to function `run_sync_in_worker_thread` is incorrect: Expected `str`, found `str | None`
- src/prefect/cli/flow_run.py:289:17: warning[possibly-missing-attribute] Attribute `state_details` may be missing on object of type `State[Any] | None | Unknown`
- src/prefect/cli/flow_run.py:290:20: warning[possibly-missing-attribute] Attribute `is_scheduled` may be missing on object of type `State[Any] | None | Unknown`
- src/prefect/cli/flow_run.py:291:22: warning[possibly-missing-attribute] Attribute `timestamp` may be missing on object of type `State[Any] | None | Unknown`
- src/prefect/cli/flow_run.py:297:21: warning[possibly-missing-attribute] Attribute `type` may be missing on object of type `State[Any] | None | Unknown`
- src/prefect/cli/flow_run.py:434:50: error[invalid-argument-type] Argument to function `load_flow_from_entrypoint` is incorrect: Expected `str`, found `str | None`
- src/prefect/cli/flow_run.py:676:35: error[invalid-argument-type] Argument to bound method `execute_flow_run` is incorrect: Expected `UUID`, found `UUID | None`
- src/prefect/cli/task_run.py:82:66: error[invalid-argument-type] Argument to function `run_sync_in_worker_thread` is incorrect: Expected `str`, found `str | None`
- src/prefect/cli/variable.py:103:29: warning[possibly-missing-attribute] Attribute `model_dump` may be missing on object of type `Variable | None`
- src/prefect/cli/work_pool.py:880:33: warning[possibly-missing-attribute] Attribute `id` may be missing on object of type `BlockSchema | None`
- src/prefect/cli/work_queue.py:47:21: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `str | None`, found `UUID | str`
+ src/prefect/cli/work_queue.py:47:21: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `str | None`, found `(UUID & ~AlwaysFalsy) | (str & ~AlwaysFalsy)`
- src/prefect/cli/work_queue.py:54:21: error[invalid-argument-type] Argument to bound method `read_work_queue_by_name` is incorrect: Expected `str`, found `UUID | str`
+ src/prefect/cli/work_queue.py:54:21: error[invalid-argument-type] Argument to bound method `read_work_queue_by_name` is incorrect: Expected `str`, found `(UUID & ~AlwaysFalsy) | (str & ~AlwaysFalsy)`
- src/prefect/cli/worker.py:158:14: error[call-non-callable] Object of type `None` is not callable
- src/prefect/events/clients.py:475:23: warning[possibly-missing-attribute] Attribute `send` may be missing on object of type `Unknown | None`
- src/prefect/events/clients.py:763:50: warning[possibly-missing-attribute] Attribute `recv` may be missing on object of type `Unknown | None`
- src/prefect/futures.py:318:22: warning[possibly-missing-attribute] Attribute `aresult` may be missing on object of type `State[R@PrefectDistributedFuture] | None | (State[Any] & ~AlwaysFalsy)`
- src/prefect/futures.py:445:22: warning[possibly-missing-attribute] Attribute `aresult` may be missing on object of type `State[R@PrefectFlowRunFuture] | None`
- src/prefect/logging/clients.py:288:50: warning[possibly-missing-attribute] Attribute `recv` may be missing on object of type `Unknown | None`
- src/prefect/utilities/_engine.py:64:13: error[invalid-argument-type] Argument to bound method `is_callback_with_parameters` is incorrect: Expected `(...) -> str`, found `(Unknown & Top[(...) -> object]) | (() -> str) | TaskRunNameCallbackWithParameters | (str & Top[(...) -> object])`
+ src/prefect/utilities/_engine.py:64:13: error[invalid-argument-type] Argument to bound method `is_callback_with_parameters` is incorrect: Expected `(...) -> str`, found `(Unknown & Top[(...) -> object]) | (str & Top[(...) -> object]) | (() -> str) | TaskRunNameCallbackWithParameters`
- Found 5481 diagnostics
+ Found 5457 diagnostics

xarray (https://github.com/pydata/xarray)
- xarray/core/dataset.py:4599:37: error[invalid-argument-type] Argument to function `either_dict_or_kwargs` is incorrect: Expected `Mapping[Any, Any] | None`, found `Hashable`
+ xarray/core/variable.py:699:66: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `(Unknown & Variable & ~Top[integer[Any]]) | _arrayfunction[Any, Any] | _arrayapi[Any, Any] | ndarray[tuple[Any, ...], dtype[Unknown]]`
+ xarray/core/variable.py:701:55: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `(Unknown & Variable & ~Top[integer[Any]]) | _arrayfunction[Any, Any] | _arrayapi[Any, Any] | ndarray[tuple[Any, ...], dtype[Unknown]]`
+ xarray/core/variable.py:704:24: warning[possibly-missing-attribute] Attribute `ndim` may be missing on object of type `(Unknown & Variable & ~Top[integer[Any]]) | _arrayfunction[Any, Any] | _arrayapi[Any, Any] | ndarray[tuple[Any, ...], dtype[Unknown]]`
+ xarray/core/variable.py:706:32: warning[possibly-missing-attribute] Attribute `ndim` may be missing on object of type `(Unknown & Variable & ~Top[integer[Any]]) | _arrayfunction[Any, Any] | _arrayapi[Any, Any] | ndarray[tuple[Any, ...], dtype[Unknown]]`
+ xarray/core/variable.py:708:43: warning[possibly-missing-attribute] Attribute `data` may be missing on object of type `(Unknown & Variable & ~Top[integer[Any]]) | _arrayfunction[Any, Any] | _arrayapi[Any, Any] | ndarray[tuple[Any, ...], dtype[Unknown]]`
- xarray/core/variable.py:719:35: warning[possibly-missing-attribute] Attribute `dims` may be missing on object of type `(Unknown & ~int & ~Top[integer[Any]] & ~slice[object, object, object]) | ndarray[tuple[Any, ...], dtype[Unknown]]`
+ xarray/core/variable.py:719:35: warning[possibly-missing-attribute] Attribute `dims` may be missing on object of type `(Unknown & Variable & ~Top[integer[Any]]) | _arrayfunction[Any, Any] | _arrayapi[Any, Any] | ndarray[tuple[Any, ...], dtype[Unknown]]`
- Found 1705 diagnostics
+ Found 1709 diagnostics

AutoSplit (https://github.com/Toufool/AutoSplit)
- src/AutoSplit.py:1027:9: warning[possibly-missing-attribute] Attribute `ignore` may be missing on object of type `QCloseEvent | None`
- Found 33 diagnostics
+ Found 32 diagnostics

aiohttp (https://github.com/aio-libs/aiohttp)
- aiohttp/web_urldispatcher.py:920:22: error[call-non-callable] Object of type `None` is not callable
- Found 195 diagnostics
+ Found 194 diagnostics

hydpy (https://github.com/hydpy-dev/hydpy)
- hydpy/auxs/calibtools.py:3442:30: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `Sequence[str] | None`
- hydpy/auxs/calibtools.py:3444:13: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `Sequence[str] | None`
- hydpy/auxs/calibtools.py:3446:16: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `Sequence[int | float] | None`
- hydpy/auxs/calibtools.py:3447:16: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `Sequence[int | float] | None`
- hydpy/auxs/calibtools.py:3448:16: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `Sequence[int | float] | None`
- hydpy/auxs/calibtools.py:3449:16: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `Sequence[str | None] | None`
- hydpy/auxs/calibtools.py:3471:41: error[invalid-argument-type] Argument to class `tuple` is incorrect: Expected `Iterable[int | float]`, found `Sequence[int | float] | None`
- hydpy/auxs/calibtools.py:3472:41: error[invalid-argument-type] Argument to class `tuple` is incorrect: Expected `Iterable[int | float]`, found `Sequence[int | float] | None`
- hydpy/auxs/calibtools.py:3473:41: error[invalid-argument-type] Argument to class `tuple` is incorrect: Expected `Iterable[int | float]`, found `Sequence[int | float] | None`
- hydpy/auxs/calibtools.py:3474:43: error[invalid-argument-type] Argument to class `tuple` is incorrect: Expected `Iterable[str | None]`, found `Sequence[str | None] | None`
- hydpy/auxs/calibtools.py:3492:9: error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `Iterable[Any]`, found `Sequence[str] | None`
- hydpy/auxs/calibtools.py:3494:9: error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `Iterable[Any]`, found `Sequence[int | float] | None | Unknown`
- hydpy/auxs/calibtools.py:3495:9: error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `Iterable[Any]`, found `Sequence[int | float] | None | Unknown`
- hydpy/auxs/calibtools.py:3496:9: error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `Iterable[Any]`, found `Sequence[int | float] | None | Unknown`
- hydpy/auxs/calibtools.py:3497:9: error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `Iterable[Any]`, found `Sequence[str | None] | None | Unknown`
- hydpy/core/auxfiletools.py:751:25: warning[possibly-missing-attribute] Attribute `__name__` may be missing on object of type `type[Parameter] | None`
- hydpy/core/auxfiletools.py:755:21: warning[possibly-missing-attribute] Attribute `__name__` may be missing on object of type `type[Parameter] | None`
- hydpy/core/filetools.py:212:16: error[invalid-return-type] Return type does not match returned value: expected `str`, found `str | None`
- hydpy/models/sw1d_network.py:1169:75: error[invalid-argument-type] Argument to bound method `append_submodel` is incorrect: Expected `RoutingModel_V1 | RoutingModel_V2`, found `RoutingModel_V1 | RoutingModel_V2 | RoutingModel_V3`
- hydpy/models/sw1d_network.py:1170:78: error[invalid-argument-type] Argument to bound method `append_submodel` is incorrect: Expected `RoutingModel_V2 | RoutingModel_V3`, found `RoutingModel_V1 | RoutingModel_V2 | RoutingModel_V3`
- hydpy/models/sw1d_network.py:1174:74: error[invalid-argument-type] Argument to bound method `append_submodel` is incorrect: Expected `RoutingModel_V2 | RoutingModel_V3`, found `RoutingModel_V1 | RoutingModel_V2 | RoutingModel_V3`
- Found 673 diagnostics
+ Found 652 diagnostics

django-stubs (https://github.com/typeddjango/django-stubs)
- mypy_django_plugin/config.py:72:25: error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `str | PathLike[str]`, found `str | None`
- Found 452 diagnostics
+ Found 451 diagnostics

dd-trace-py (https://github.com/DataDog/dd-trace-py)
- .gitlab/validate-ddtrace-package.py:168:66: error[invalid-argument-type] Argument to function `validate_sdist` is incorrect: Expected `str`, found `str | None`
- .gitlab/validate-ddtrace-package.py:192:39: error[invalid-argument-type] Argument to function `build_expected_set` is incorrect: Expected `str`, found `str | None`
- .gitlab/validate-ddtrace-package.py:212:66: error[invalid-argument-type] Argument to function `identify_version_mismatches` is incorrect: Expected `str`, found `str | None`
- ddtrace/_trace/processor/__init__.py:170:43: error[unsupported-operator] Operator `<=` is not supported between objects of type `int | float | None` and `Literal[0]`
- ddtrace/commands/ddtrace_run.py:131:8: error[no-matching-overload] No overload of function `basename` matches arguments
- ddtrace/commands/ddtrace_run.py:143:18: error[invalid-argument-type] Argument to function `execl` is incorrect: Expected `str | bytes | PathLike[str] | PathLike[bytes]`, found `None | str`
- ddtrace/commands/ddtrace_run.py:143:43: warning[possibly-missing-attribute] Attribute `command` may be missing on object of type `None | Unknown`
- tests/tracer/test_span.py:193:29: error[invalid-argument-type] Argument to bound method `set_metric` is incorrect: Expected `int | float`, found `str | Unknown | None | ... omitted 6 union elements`
+ tests/tracer/test_span.py:193:29: error[invalid-argument-type] Argument to bound method `set_metric` is incorrect: Expected `int | float`, found `Span | Unknown | None | ... omitted 6 union elements`
- Found 8740 diagnostics
+ Found 8733 diagnostics

CPython (Argument Clinic) (https://github.com/python/cpython)
- Tools/clinic/libclinic/app.py:173:16: error[invalid-return-type] Return type does not match returned value: expected `Destination`, found `Destination | None`
- Tools/clinic/libclinic/dsl_parser.py:376:13: error[no-matching-overload] No overload of bound method `update` matches arguments
- Tools/clinic/libclinic/dsl_parser.py:1159:21: error[invalid-assignment] Invalid subscript assignment with key of type `str | None` and value of type `Any` on object of type `dict[str, Any]`
- Found 21 diagnostics
+ Found 18 diagnostics

cryptography (https://github.com/pyca/cryptography)
- tests/wycheproof/test_ecdh.py:72:54: error[invalid-argument-type] Argument to function `derive_private_key` is incorrect: Expected `EllipticCurve`, found `EllipticCurve | None`
- tests/wycheproof/test_rsa.py:162:44: warning[possibly-missing-attribute] Attribute `digest_size` may be missing on object of type `(HashAlgorithm & ~SHA1) | None`
- tests/wycheproof/test_rsa.py:181:34: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `HashAlgorithm`, found `HashAlgorithm | None`
- tests/wycheproof/test_rsa.py:192:38: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `HashAlgorithm`, found `HashAlgorithm | None`
- Found 37 diagnostics
+ Found 33 diagnostics

bokeh (https://github.com/bokeh/bokeh)
- src/bokeh/core/property/wrappers.py:568:21: error[invalid-assignment] Cannot assign to a subscript on an object of type `Sequence[Any]`
+ src/bokeh/core/property/wrappers.py:571:29: error[invalid-argument-type] Method `__getitem__` of type `Overload[(index: int) -> object, (index: slice[int | None, int | None, int | None]) -> Top[MutableSequence[Unknown]]]` cannot be called with key of type `tuple[@Todo, ...]` on object of type `Top[MutableSequence[Unknown]]`
- src/bokeh/core/serialization.py:660:40: error[no-matching-overload] No overload of function `__new__` matches arguments
- src/bokeh/core/serialization.py:709:9: error[invalid-assignment] Invalid subscript assignment with key of type `ID` and value of type `Model | None` on object of type `dict[ID, Model]`
- src/bokeh/core/serialization.py:709:26: warning[possibly-missing-attribute] Attribute `id` may be missing on object of type `Model | None`
- src/bokeh/core/serialization.py:714:16: warning[possibly-missing-attribute] Attribute `_initialized` may be missing on object of type `Model | None`
- src/bokeh/core/serialization.py:721:17: warning[possibly-missing-attribute] Attribute `set_from_json` may be missing on object of type `Model | None`
- src/bokeh/core/serialization.py:723:16: error[invalid-return-type] Return type does not match returned value: expected `Model`, found `Model | None`
- src/bokeh/events.py:202:17: error[call-non-callable] Object of type `None` is not callable
- Found 847 diagnostics
+ Found 840 diagnostics

egglog-python (https://github.com/egraphs-good/egglog-python)
- python/egglog/pretty.py:378:9: error[type-assertion-failure] Type `RulesetDecl | CombinedRulesetDecl | RewriteDecl | ... omitted 27 union elements` is not equivalent to `Never`
+ python/egglog/pretty.py:378:9: error[type-assertion-failure] Type `PartialCallDecl & ~LitDecl & ~UnboundVarDecl & ~LetRefDecl & ~CallDecl & ~PyObjectDecl & ~ActionCommandDecl & ~RewriteDecl & ~BiRewriteDecl & ~RuleDecl & ~SetDecl & ~UnionDecl & ~LetDecl & ~ExprActionDecl & ~ExprFactDecl & ~ChangeDecl & ~PanicDecl & ~SetCostDecl & ~EqDecl & ~RulesetDecl & ~CombinedRulesetDecl & ~SaturateDecl & ~RepeatDecl & ~SequenceDecl & ~LetSchedulerDecl & ~RunDecl & ~DefaultRewriteDecl & ~BackOffDecl & ~ValueDecl & ~GetCostDecl` is not equivalent to `Never`

streamlit (https://github.com/streamlit/streamlit)
+ lib/streamlit/runtime/metrics_util.py:312:31: warning[unused-ignore-comment] Unused `ty: ignore` directive
- Found 11 diagnostics
+ Found 12 diagnostics

sympy (https://github.com/sympy/sympy)
- sympy/algebras/quaternion.py:132:22: error[invalid-assignment] Object of type `Basic | int | float | complex | Any` is not assignable to `Expr | int | float | complex`
+ sympy/algebras/quaternion.py:132:22: error[invalid-assignment] Object of type `Basic` is not assignable to `Expr | int | float | complex`
- sympy/algebras/quaternion.py:132:22: error[invalid-assignment] Object of type `Basic | int | float | complex | Any` is not assignable to `Expr | int | floa

... (truncated 544 lines) ...

Replace sorted-list-based narrowing constraints (AND-only) with TDD
(Ternary Decision Diagram) nodes that support AND, OR, and NOT. This
changes the merge operation from intersection to OR, so narrowing from
non-terminal branches is properly preserved after if/elif/else.

Additionally, when a branch contains a NoReturn call (e.g. sys.exit()),
gate the narrowing constraints by the ReturnsNever predicate. During
the narrowing TDD walk, non-narrowing predicates are evaluated to
determine reachability, so NoReturn branches contribute Never (absorbed
by union) rather than polluting the merged type.

Fixes astral-sh/ty#690
Fixes astral-sh/ty#685

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@alex alex force-pushed the alex/ty-narrowing-conditional-terminals branch from 970a77d to 2f7e96e Compare February 6, 2026 01:46
@alex
Copy link
Contributor Author

alex commented Feb 6, 2026

Hard to know exactly how to verify the mypy_primer results, but they appear to be overwhelmingly getting rid of "no attribute on X | None" which is basically what you'd expect form better narrowing propagation.

@carljm
Copy link
Contributor

carljm commented Feb 6, 2026

Hard to know exactly how to verify the mypy_primer results, but they appear to be overwhelmingly getting rid of "no attribute on X | None" which is basically what you'd expect form better narrowing propagation.

We've currently got some non-determinism we're working to fix, so the mypy-primer results are particularly difficult to parse right now -- some projects (sympy, prefect) will have a lot of totally unrelated changes in the diffs. The results look very good to me overall.

@astral-sh-bot
Copy link

astral-sh-bot bot commented Feb 6, 2026

ecosystem-analyzer results

Lint rule Added Removed Changed
possibly-missing-attribute 14 118 21
invalid-argument-type 4 98 33
invalid-return-type 0 32 4
not-subscriptable 0 20 0
unsupported-operator 0 17 3
invalid-assignment 0 19 0
no-matching-overload 0 11 0
unresolved-attribute 0 10 0
call-non-callable 0 7 0
not-iterable 0 5 1
type-assertion-failure 1 0 5
unused-type-ignore-comment 2 0 0
invalid-type-form 0 1 0
unused-ignore-comment 1 0 0
Total 22 338 67

Full report with detailed diff (timing results)

@carljm
Copy link
Contributor

carljm commented Feb 6, 2026

(Probably not ready for review yet, just wanted to see the ecosystem results.)

You can also open a draft PR and you'll still get the CI results. Lmk if you want an early review on this now, if not maybe mark it draft until you do.

Super exciting, thank you for taking this on!

@alex
Copy link
Contributor Author

alex commented Feb 6, 2026

An early review would be appreciated, thanks! Particularly if there's test cases you think it's missing.

@sharkdp sharkdp self-assigned this Feb 6, 2026
@sharkdp
Copy link
Contributor

sharkdp commented Feb 6, 2026

This looks really great, thank you!

I only did a coarse review as I understand that this is not quite finished.

It's fantastic to see that we can make use of the existing TDD for narrowing constraints as well. I think we should eventually rename some types/functions (reachability constraints => some new term), if we go through with this, but it's probably better to do this in a follow-up to keep the diff small.

Particularly if there's test cases you think it's missing.

One test case that I found that is still not handled with this PR is the following:

def _(x: int | None):
    if 1 + 1 == 2:
        if x is None:
            return
        reveal_type(x)  # revealed: int

    reveal_type(x)  # revealed: `int | None`, should ideally be `int`

Here, 1 + 1 == 2 is a placeholder for any nontrivial condition that evaluates to Literal[True], like sys.version_info >= (3, 9) on a recent Python version. If you replace it with True, it works fine. Handling this would probably involve adding new nodes to the narrowing constraints TDD that would later evaluate the truthiness of the test condition? This seems like a rather obscure edge case, so I'm not sure if it's worth adding a lot of complexity.

@alex
Copy link
Contributor Author

alex commented Feb 6, 2026

Interesting, in general those sorts of compile time values feel like they though to be basically the same as Literal[True] for all purposes. I'll see what it'd take to implement (depending on what it looks like, I might conclude one of: a) it proves that some of the abstractions in this PR are wrong, b) it's easy, just include, c) it's still annoying, defer to follow up).

@carljm
Copy link
Contributor

carljm commented Feb 6, 2026

Interesting, in general those sorts of compile time values feel like they though to be basically the same as Literal[True] for all purposes.

That did used to be true in ty (they were all handled via the reachability-constraints mechanism), but then we added special-cased earlier handling of "simple" statically-known conditions that don't require type inference to resolve. We did that because it was a significant performance boost -- but it does mean that it's now easy to accidentally introduce different behavior for the two cases.

Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

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

Similar to @sharkdp 's suggested test, but slightly different (though I suspect with the same fix) is the OP example in astral-sh/ty#685 but modified to use a condition that (currently) requires type inference:

def _(val: int | None):
  if val is None:
    if 1+1+2:
      return

  reveal_type(val) # revealed: int | None, should be just `int`

(Of course "requires type inference" is slippery here -- we could certainly fix this by implementing direct support for more constexprs like this in semantic indexing. The real example here would be something like importing a Final constant from another module; we can't resolve that in semantic indexing, we need a reachability constraint to be resolved later.)


# Only the if-branch (A) and elif-branch (B) flow through.
# The else-branch returned, so its narrowing doesn't participate.
reveal_type(x) # revealed: B | (A & ~B)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm a bit confused about why this isn't A | (B & ~A). If we flow through the if we should just get A, if we flow through the elif we have B & ~A -- adding reveal_type(x) inside the branches above confirms. So it's not clear to me why we don't end up with the union of those here.

I guess those are equivalent types (and also equivalent to just A | B?) -- so maybe this is an acceptable (if slightly surprising) transformation that happens due to the TDD implementation?

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess those are equivalent types (and also equivalent to just A | B?) -- so maybe this is an acceptable (if slightly surprising) transformation that happens due to the TDD implementation?

Yes, the root cause of this is the reversed ordering of TDD nodes that we introduced in #20098.

This didn't have any observable side effects for reachability constraints (which evaluate to a ternary answer), but it does play a role for narrowing constraints.

Removing that .reverse() changes the result here to A | (B & ~A). But it also breaks ~20 other tests. Some of them are just ordering related, but in some other cases, some union-builder simplifications do not seem to trigger which is a bit concerning. I have not investigated more closely, as we probably still need that optimization.

return

# Only the elif-B and elif-C branches flow through.
reveal_type(x) # revealed: (C & ~A) | (B & ~A & ~C)
Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly here I'd naively expect (B & ~A) | (C & ~A & ~B) -- but they are equivalent. I'm just curious why it happens.

@carljm
Copy link
Contributor

carljm commented Feb 6, 2026

We could also add some tests using match statements; one example (could be simplified) in astral-sh/ty#1384

@carljm
Copy link
Contributor

carljm commented Feb 6, 2026

Took a pass on the tests -- going to have @sharkdp take the lead on review here.

Record if-statement conditions as narrowing constraints for all places
in scope, not just those directly narrowed by the condition. This ensures
that unreachable branches (e.g., the else-branch of `if 1 + 1 == 2:`)
contribute `Never` rather than diluting narrowing from reachable branches.

Uses the same ScopedPredicateId for both specific-places and all-places
recording, so TDD atoms are shared via hash-consing. For places already
narrowed by the condition, AND(atom, atom) = atom — no duplication.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@alex alex force-pushed the alex/ty-narrowing-conditional-terminals branch from 1783c0a to ada1084 Compare February 6, 2026 15:54
Add tests for:
- Always-true condition nested inside a narrowing branch
- Match statement with terminal default branch (class patterns)
- Match statement with reassignment and terminal default (issue astral-sh#1384)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codspeed-hq
Copy link

codspeed-hq bot commented Feb 6, 2026

Merging this PR will not alter performance

✅ 24 untouched benchmarks
⏩ 30 skipped benchmarks1


Comparing alex:alex/ty-narrowing-conditional-terminals (9d8c67a) with main (90c8571)

Open in CodSpeed

Footnotes

  1. 30 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@AlexWaygood

This comment was marked as resolved.

@alex

This comment was marked as resolved.

Revert the all-places narrowing propagation introduced in the previous
two commits, as it causes exponential blowup on long if/elif chains
(e.g. black/nodes.py with 16 conditions). Keep the test cases but
update expectations to reflect the current behavior, with TODO comments
for future improvement.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@alex
Copy link
Contributor Author

alex commented Feb 6, 2026

Ok, ended up rolling back the fix for if 1+1 == 2 propagation due to exponential blow up.

@alex
Copy link
Contributor Author

alex commented Feb 7, 2026

I think this is ready for review:

  1. The implementation I had for solving the if 1 + 1 == 2 branching issue had serious performance problems. Conceptually I feel like this should be possible (the branch is statically known, that's the point!), but given the obvious impl didn't work, I think it makes more sense to defer.
  2. There's still some smaller performance regressions on 3 of the benchmarks. I don't feel well calibrated for to what extent that's expected and/or acceptabled.
  3. My review of the mypy_primer results looks ok. I can't say that I scruitinized every single one, but I skimmed the list and looked at any that seemed interesting/questionable and they all looked either correct, or like an orthogonal issue.
  4. It does seem like some sort of normalization would be valuable (e.g., (A & ~B) | (B & ~A) really should just be A | B), but I think that's basically a consequence of the existing TDD impl/intersection builder, and thus shouldn't be part of this PR. Let me know if you disagree.

@AlexWaygood

This comment was marked as resolved.

@AlexWaygood

This comment was marked as resolved.

@alex

This comment was marked as resolved.

@carljm

This comment was marked as resolved.

@alex

This comment was marked as resolved.

@carljm

This comment was marked as resolved.

@alex

This comment was marked as resolved.

@TomerBin
Copy link
Contributor

TomerBin commented Feb 7, 2026

Amazing 😍
Can't wait to keep working on astral-sh/ty#626 once this is landed

Copy link
Contributor

@sharkdp sharkdp left a comment

Choose a reason for hiding this comment

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

Thank you very much for working on this.

I feel like I now have an understanding of how this approach works, so I'm submitting a first review with some higher-level questions, before doing another final pass.

Comment on lines +993 to +996
/// Uses TDD-level negation (`add_not_constraint`) rather than creating a new predicate atom
/// for the negated predicate. This ensures that `atom(P) OR NOT(atom(P))` simplifies to
/// `ALWAYS_TRUE` in the TDD, so narrowing is correctly cancelled out after complete
/// if/else blocks.
Copy link
Contributor

Choose a reason for hiding this comment

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

So this makes me wonder if we can completely replace the is_positive flag in predicates and replace it with NOT(…) from the TDD? With the change here, we now use P with is_positive: false for some situations and NOT(P) for other situations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think yes, it is. But I don't want to make this PR too large, it's already annoyingly large :-( And I don't think the inverse is true, I don't think we could use is_positive for this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, I'll look into this in a follow-up.

Comment on lines +775 to +779
/// The "ambiguous" branch in the TDD is not followed for narrowing purposes, because
/// narrowing constraints record which predicates hold along the control flow path.
/// The predicates may be statically ambiguous (we can't determine their truthiness
/// at analysis time), but they still hold dynamically at runtime and should be used
/// for narrowing.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this means that a BDD would in principle be sufficient for the narrowing constraints?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think so, we're just piggy backing on the TDD from the constraints.

Comment on lines +877 to +905
// True branch: predicate holds → accumulate positive narrowing
let true_accumulated =
accumulate_constraint(db, accumulated.clone(), pos_constraint);
let true_ty = self.narrow_by_constraint_inner(
db,
predicates,
node.if_true,
base_ty,
place,
true_accumulated,
);

// False branch: predicate doesn't hold → accumulate negative narrowing
let neg_predicate = Predicate {
node: predicate.node,
is_positive: !predicate.is_positive,
};
let neg_constraint = infer_narrowing_constraint(db, neg_predicate, place);
let false_accumulated = accumulate_constraint(db, accumulated, neg_constraint);
let false_ty = self.narrow_by_constraint_inner(
db,
predicates,
node.if_false,
base_ty,
place,
false_accumulated,
);

UnionType::from_elements(db, [true_ty, false_ty])
Copy link
Contributor

Choose a reason for hiding this comment

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

It feels like this would benefit from some fast-paths for AlwaysFalse nodes? Otherwise, we'll build a lot of unions with Never.

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this helped a lot with performance — all regressions are basically gone now.

- Use A/B/C class aliases in sequential if-statement tests for consistency
- Remove distracting `y` assignments, use `pass` instead
- Rename `val` to `x` for symmetry with adjacent test
- Replace wasteful `infer_narrowing_constraint` calls with direct
  `matches!(predicate.node, PredicateNode::ReturnsNever(_))` check
- Add fast-paths to skip ALWAYS_FALSE branches in narrow_by_constraint_inner
- Mark Ambiguous arm as unreachable for ReturnsNever predicates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@alex alex requested a review from ibraheemdev as a code owner February 11, 2026 23:36
@astral-sh-bot
Copy link

astral-sh-bot bot commented Feb 11, 2026

Memory usage report

Summary

Project Old New Diff Outcome
prefect 680.31MB 681.12MB +0.12% (822.87kB)
sphinx 270.83MB 271.24MB +0.15% (422.34kB)
trio 118.05MB 118.20MB +0.13% (152.99kB)
flake8 49.50MB 49.36MB -0.28% (142.44kB) ⬇️

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
all_narrowing_constraints_for_expression 3.48MB 4.41MB +26.99% (960.54kB)
semantic_index 183.38MB 182.72MB -0.36% (675.06kB)
all_negative_narrowing_constraints_for_expression 1.51MB 1.65MB +9.70% (149.52kB)
infer_expression_types_impl 51.25MB 51.38MB +0.26% (136.80kB)
infer_definition_types 82.99MB 83.07MB +0.09% (76.19kB)
infer_expression_type_impl 10.32MB 10.38MB +0.64% (67.87kB)
StaticClassLiteral<'db>::implicit_attribute_inner_ 9.82MB 9.85MB +0.28% (28.54kB)
infer_scope_types_impl 57.27MB 57.28MB +0.02% (12.23kB)
Type<'db>::member_lookup_with_policy_ 15.28MB 15.29MB +0.07% (11.00kB)
Type<'db>::class_member_with_policy_ 16.80MB 16.81MB +0.06% (10.35kB)
GenericAlias<'db>::variance_of_ 558.98kB 567.89kB +1.59% (8.91kB)
is_redundant_with_impl 5.92MB 5.93MB +0.14% (8.40kB)
IntersectionType 2.49MB 2.50MB +0.32% (8.19kB)
is_redundant_with_impl::interned_arguments 6.16MB 6.17MB +0.12% (7.48kB)
check_file_impl 16.62MB 16.61MB -0.03% (5.76kB)
... 13 more

sphinx

Name Old New Diff Outcome
all_narrowing_constraints_for_expression 1.06MB 1.46MB +37.78% (408.86kB)
semantic_index 68.46MB 68.31MB -0.22% (151.07kB)
infer_expression_types_impl 21.53MB 21.59MB +0.27% (59.32kB)
IntersectionType 1.01MB 1.04MB +2.74% (28.38kB)
infer_definition_types 23.39MB 23.41MB +0.10% (23.71kB)
is_redundant_with_impl::interned_arguments 2.75MB 2.76MB +0.39% (11.09kB)
is_redundant_with_impl 2.18MB 2.19MB +0.48% (10.70kB)
infer_scope_types_impl 17.45MB 17.46MB +0.06% (10.68kB)
all_negative_narrowing_constraints_for_expression 634.72kB 644.55kB +1.55% (9.84kB)
UnionType 1.65MB 1.66MB +0.14% (2.31kB)
Type<'db>::class_member_with_policy_ 7.84MB 7.84MB +0.01% (1.18kB)
Type<'db>::try_call_dunder_get_ 5.49MB 5.49MB +0.02% (1.10kB)
Type<'db>::class_member_with_policy_::interned_arguments 3.99MB 3.99MB +0.01% (624.00B)
FunctionType<'db>::signature_ 2.40MB 2.40MB +0.02% (616.00B)
FunctionType 3.37MB 3.37MB +0.02% (592.00B)
... 21 more

trio

Name Old New Diff Outcome
semantic_index 33.01MB 32.87MB -0.42% (142.39kB)
all_narrowing_constraints_for_expression 251.02kB 357.11kB +42.26% (106.09kB)
infer_expression_types_impl 5.61MB 5.66MB +0.88% (50.78kB)
is_redundant_with_impl::interned_arguments 570.71kB 613.16kB +7.44% (42.45kB)
is_redundant_with_impl 490.27kB 514.17kB +4.88% (23.91kB)
Type<'db>::class_member_with_policy_ 2.05MB 2.07MB +0.97% (20.39kB)
infer_definition_types 7.17MB 7.18MB +0.20% (14.66kB)
Type<'db>::class_member_with_policy_::interned_arguments 1.10MB 1.11MB +0.62% (7.01kB)
Type<'db>::member_lookup_with_policy_::interned_arguments 850.18kB 856.58kB +0.75% (6.40kB)
Type<'db>::member_lookup_with_policy_ 1.76MB 1.76MB +0.35% (6.32kB)
IntersectionType 243.81kB 248.47kB +1.91% (4.66kB)
all_negative_narrowing_constraints_for_expression 109.96kB 113.71kB +3.40% (3.74kB)
infer_scope_types_impl 5.45MB 5.45MB +0.05% (2.57kB)
UnionType 345.50kB 346.97kB +0.43% (1.47kB)
check_file_impl 1.92MB 1.92MB -0.07% (1.40kB)
... 18 more

flake8

Name Old New Diff Outcome
semantic_index 15.40MB 15.24MB -1.04% (163.41kB) ⬇️
all_narrowing_constraints_for_expression 37.21kB 54.55kB +46.59% (17.34kB) ⬇️
infer_expression_types_impl 1.06MB 1.06MB +0.15% (1.62kB) ⬇️
is_redundant_with_impl::interned_arguments 152.80kB 153.31kB +0.34% (528.00B) ⬇️
is_redundant_with_impl 148.55kB 149.06kB +0.35% (528.00B) ⬇️
IntersectionType 81.07kB 81.43kB +0.44% (368.00B) ⬇️
infer_scope_types_impl 1.08MB 1.08MB +0.03% (336.00B) ⬇️
infer_definition_types 1.85MB 1.85MB +0.02% (300.00B) ⬇️

@alex

This comment was marked as resolved.

@ibraheemdev

This comment was marked as resolved.

@sharkdp sharkdp merged commit a928da0 into astral-sh:main Feb 12, 2026
50 checks passed
@alex
Copy link
Contributor Author

alex commented Feb 12, 2026

Whoo! Thanks so much!

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

Labels

ecosystem-analyzer ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

better narrowing from conditional terminals and NoReturn calls Incomplete narrowing with terminal statement inside nested ifs

7 participants