Skip to content

[ty] Infer definite equality comparison results#26290

Merged
charliermarsh merged 2 commits into
mainfrom
charlie/infer-equality-comparison-results
Jun 24, 2026
Merged

[ty] Infer definite equality comparison results#26290
charliermarsh merged 2 commits into
mainfrom
charlie/infer-equality-comparison-results

Conversation

@charliermarsh

Copy link
Copy Markdown
Member

Summary

The equality rewrite in #25788 centralized runtime == and != reasoning for narrowing and match reachability, but ordinary comparison inference continued to use separate literal and dunder logic. As a result, two disjoint enum domains could narrow correctly while the comparison expression itself remained bool, keeping an impossible branch reachable.

This routes definite equality and inequality truthiness through the shared comparison evaluator before falling back to the existing inference path. Ambiguous comparisons still use normal dunder inference, preserving custom non-boolean return types and the independence of __eq__ and __ne__.

from enum import Enum
from typing import Literal

class Choice(str, Enum):
    FIRST = "first"
    SECOND = "second"
    THIRD = "third"
    FOURTH = "fourth"

def compare(
    left: Literal[Choice.FIRST, Choice.SECOND],
    right: Literal[Choice.THIRD, Choice.FOURTH],
):
    reveal_type(left == right)  # Literal[False]
    reveal_type(left != right)  # Literal[True]

This also resolves existing tuple comparison TODOs whose mismatched literal elements are already known to compare unequal. With the behavior provided here, #26270 can remain responsible for avoiding same-enum domain expansion without changing comparison results.

@astral-sh-bot astral-sh-bot Bot added the ty Multi-file analysis & type inference label Jun 24, 2026
@astral-sh-bot

astral-sh-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 94.37%. The percentage of expected errors that received a diagnostic held steady at 89.00%. The number of fully passing files held steady at 94/134.

@astral-sh-bot

astral-sh-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown

Memory usage report

Summary

Project Old New Diff Outcome
prefect 516.30MB 516.70MB +0.08% (410.58kB)
sphinx 193.37MB 193.67MB +0.15% (303.71kB)
trio 77.51MB 77.59MB +0.10% (82.53kB)
flake8 30.75MB 30.77MB +0.08% (24.80kB)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
infer_expression_types_impl 55.93MB 56.19MB +0.47% (269.34kB)
member_lookup_with_policy_inner 14.69MB 14.71MB +0.13% (19.63kB)
Type<'db>::class_member_with_policy_ 10.93MB 10.95MB +0.16% (17.45kB)
IntersectionType 969.20kB 980.56kB +1.17% (11.37kB)
UnionType 1.06MB 1.07MB +0.88% (9.55kB)
TupleType<'db>::to_class_type_ 476.76kB 486.16kB +1.97% (9.39kB)
infer_definition_types 69.15MB 69.15MB +0.01% (9.32kB)
member_lookup_with_policy_inner::interned_arguments 6.72MB 6.73MB +0.11% (7.38kB)
infer_scope_types_impl 38.84MB 38.85MB +0.02% (7.09kB)
Type<'db>::apply_specialization_inner_ 3.02MB 3.03MB +0.23% (7.02kB)
Specialization 2.66MB 2.67MB +0.24% (6.67kB)
Type<'db>::class_member_with_policy_::interned_arguments 5.72MB 5.73MB +0.11% (6.40kB)
parsed_module 19.35MB 19.36MB +0.03% (5.88kB)
is_redundant_with_impl::interned_arguments 2.33MB 2.34MB +0.21% (4.98kB)
Type<'db>::apply_specialization_inner_::interned_arguments 2.94MB 2.95MB +0.16% (4.69kB)
... 23 more

sphinx

Name Old New Diff Outcome
infer_expression_types_impl 22.98MB 23.10MB +0.52% (123.39kB)
member_lookup_with_policy_inner 6.10MB 6.13MB +0.55% (34.41kB)
Type<'db>::class_member_with_policy_ 5.07MB 5.10MB +0.63% (32.93kB)
TupleType<'db>::to_class_type_ 161.49kB 176.39kB +9.23% (14.90kB)
member_lookup_with_policy_inner::interned_arguments 2.69MB 2.70MB +0.53% (14.53kB)
Type<'db>::apply_specialization_inner_ 1.52MB 1.53MB +0.83% (12.86kB)
UnionType 567.89kB 580.66kB +2.25% (12.77kB)
Type<'db>::class_member_with_policy_::interned_arguments 2.35MB 2.36MB +0.52% (12.59kB)
Specialization 1.40MB 1.41MB +0.68% (9.73kB)
Type<'db>::apply_specialization_inner_::interned_arguments 1.54MB 1.54MB +0.55% (8.67kB)
GenericAlias 612.42kB 618.75kB +1.03% (6.33kB)
infer_definition_types 19.55MB 19.55MB +0.03% (5.45kB)
IntersectionType 547.48kB 552.90kB +0.99% (5.42kB)
StaticClassLiteral<'db>::try_mro_ 2.48MB 2.48MB -0.18% (4.47kB)
is_redundant_with_impl 901.58kB 905.26kB +0.41% (3.68kB)
... 14 more

trio

Name Old New Diff Outcome
infer_expression_types_impl 6.51MB 6.55MB +0.61% (40.52kB)
member_lookup_with_policy_inner 1.52MB 1.53MB +0.56% (8.72kB)
Type<'db>::apply_specialization_inner_ 509.12kB 513.09kB +0.78% (3.96kB)
UnionType 142.09kB 145.98kB +2.74% (3.89kB)
TupleType<'db>::to_class_type_ 43.69kB 47.32kB +8.32% (3.63kB)
infer_definition_types 5.59MB 5.59MB +0.06% (3.26kB)
Type<'db>::apply_specialization_inner_::interned_arguments 504.06kB 506.80kB +0.54% (2.73kB)
Type<'db>::class_member_with_policy_ 1.25MB 1.25MB +0.20% (2.59kB)
BoundMethodType 182.73kB 180.16kB -1.41% (2.58kB)
Specialization 456.30kB 458.59kB +0.50% (2.30kB)
IntersectionType 133.91kB 135.58kB +1.25% (1.67kB)
is_redundant_with_impl::interned_arguments 218.71kB 220.34kB +0.75% (1.63kB)
GenericAlias 187.38kB 188.86kB +0.79% (1.48kB)
is_redundant_with_impl 171.77kB 173.21kB +0.84% (1.44kB)
parsed_module 15.04MB 15.04MB +0.01% (1016.00B)
... 11 more

flake8

Name Old New Diff Outcome
infer_expression_types_impl 960.88kB 967.57kB +0.70% (6.69kB)
parsed_module 9.77MB 9.78MB +0.06% (5.77kB)
member_lookup_with_policy_inner 436.04kB 439.35kB +0.76% (3.30kB)
Type<'db>::class_member_with_policy_ 348.15kB 351.38kB +0.93% (3.23kB)
member_lookup_with_policy_inner::interned_arguments 204.26kB 205.66kB +0.69% (1.41kB)
TupleType<'db>::to_class_type_ 13.84kB 15.12kB +9.23% (1.28kB)
Type<'db>::class_member_with_policy_::interned_arguments 167.98kB 169.20kB +0.73% (1.22kB)
infer_definition_types 1.35MB 1.35MB -0.08% (1.08kB)
Type<'db>::apply_specialization_inner_ 172.01kB 173.09kB +0.62% (1.07kB)
Specialization 174.45kB 175.33kB +0.50% (896.00B)
GenericAlias 75.87kB 76.50kB +0.83% (648.00B)
Type<'db>::apply_specialization_inner_::interned_arguments 173.83kB 174.38kB +0.31% (560.00B)
UnionType 56.66kB 57.20kB +0.97% (560.00B)
all_narrowing_constraints_for_expression 87.14kB 86.72kB -0.48% (432.00B)
loop_header_reachability 12.43kB 12.05kB -3.02% (384.00B)
... 8 more

@astral-sh-bot

astral-sh-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-argument-type 0 16 0
invalid-key 0 7 0
unresolved-attribute 0 5 0
invalid-assignment 0 2 0
invalid-return-type 0 1 0
possibly-unresolved-reference 0 1 0
unsupported-operator 0 1 0
Total 0 33 0

Flaky changes detected. This PR summary excludes flaky changes; see the HTML report for details.

Raw diff (33 changes)
cloud-init (https://github.com/canonical/cloud-init)
- tests/unittests/distros/test_opensuse.py:287:16 error[unresolved-attribute] Object of type `Distro` has no attribute `read_only_root`
- tests/unittests/distros/test_opensuse.py:308:16 error[unresolved-attribute] Object of type `Distro` has no attribute `read_only_root`
- tests/unittests/distros/test_opensuse.py:329:16 error[unresolved-attribute] Object of type `Distro` has no attribute `read_only_root`

discord.py (https://github.com/Rapptz/discord.py)
- discord/audit_logs.py:210:12 error[invalid-return-type] Return type does not match returned value: expected `list[tuple[Object, PermissionOverwrite]]`, found `list[tuple[Role | Member | User | Object, PermissionOverwrite]]`

freqtrade (https://github.com/freqtrade/freqtrade)
- freqtrade/rpc/telegram.py:579:59 error[invalid-key] Unknown key "status" for TypedDict `RPCAnalyzedDFMsg` (subscripted object has type `RPCStatusMsg | RPCStrategyMsg | RPCProtectionMsg | ... omitted 5 union elements`)
- freqtrade/rpc/telegram.py:579:59 error[invalid-key] Unknown key "status" for TypedDict `RPCEntryMsg` (subscripted object has type `RPCStatusMsg | RPCStrategyMsg | RPCProtectionMsg | ... omitted 5 union elements`)
- freqtrade/rpc/telegram.py:579:59 error[invalid-key] Unknown key "status" for TypedDict `RPCExitMsg` (subscripted object has type `RPCStatusMsg | RPCStrategyMsg | RPCProtectionMsg | ... omitted 5 union elements`)
- freqtrade/rpc/telegram.py:579:59 error[invalid-key] Unknown key "status" for TypedDict `RPCNewCandleMsg` (subscripted object has type `RPCStatusMsg | RPCStrategyMsg | RPCProtectionMsg | ... omitted 5 union elements`)
- freqtrade/rpc/telegram.py:579:59 error[invalid-key] Unknown key "status" for TypedDict `RPCProtectionMsg` (subscripted object has type `RPCStatusMsg | RPCStrategyMsg | RPCProtectionMsg | ... omitted 5 union elements`)
- freqtrade/rpc/telegram.py:579:59 error[invalid-key] Unknown key "status" for TypedDict `RPCStrategyMsg` (subscripted object has type `RPCStatusMsg | RPCStrategyMsg | RPCProtectionMsg | ... omitted 5 union elements`)
- freqtrade/rpc/telegram.py:579:59 error[invalid-key] Unknown key "status" for TypedDict `RPCWhitelistMsg` (subscripted object has type `RPCStatusMsg | RPCStrategyMsg | RPCProtectionMsg | ... omitted 5 union elements`)

psycopg (https://github.com/psycopg/psycopg)
- tests/test_cursor_common.py:159:24 error[unresolved-attribute] Module `psycopg` has no member `ProgrammingError`
- tests/test_cursor_common_async.py:157:24 error[unresolved-attribute] Module `psycopg` has no member `ProgrammingError`

pycryptodome (https://github.com/Legrandin/pycryptodome)
- lib/Crypto/IO/PKCS8.py:210:33 error[invalid-argument-type] Argument to bound method `DerSequence.decode` is incorrect: Expected `bytes`, found `None`
- lib/Crypto/IO/PKCS8.py:210:45 error[invalid-argument-type] Argument to bound method `DerSequence.decode` is incorrect: Expected `int | None`, found `tuple[Literal[1], Literal[2]]`
- lib/Crypto/IO/PKCS8.py:211:37 error[invalid-argument-type] Argument to bound method `DerObjectId.decode` is incorrect: Expected `bytes`, found `None`
- lib/Crypto/IO/PKCS8.py:216:30 error[invalid-argument-type] Argument to bound method `DerObject.decode` is incorrect: Expected `bytes`, found `None`
- lib/Crypto/IO/PKCS8.py:222:43 error[invalid-argument-type] Argument to bound method `DerObject.decode` is incorrect: Expected `bytes`, found `None`
- lib/Crypto/PublicKey/ECC.py:833:44 error[invalid-argument-type] Argument to bound method `DerObject.decode` is incorrect: Expected `bytes`, found `None`
- lib/Crypto/PublicKey/ECC.py:840:57 error[invalid-argument-type] Argument to bound method `DerObjectId.decode` is incorrect: Expected `bytes`, found `None`
- lib/Crypto/PublicKey/ECC.py:865:62 error[invalid-argument-type] Argument to bound method `DerBitString.decode` is incorrect: Expected `bytes`, found `None`
- lib/Crypto/PublicKey/RSA.py:700:22 error[invalid-argument-type] Method `__getitem__` of type `bound method DerSequence.__getitem__(n: int) -> None` cannot be called with key of type `slice[Literal[1], Literal[6], None]` on object of type `DerSequence`
- lib/Crypto/PublicKey/RSA.py:700:22 error[unsupported-operator] Operator `+` is not supported between objects of type `None` and `list[IntegerBase]`
- lib/Crypto/PublicKey/RSA.py:700:42 error[invalid-argument-type] Argument to `IntegerBase.__init__` is incorrect: Expected `IntegerBase | int`, found `None`
- lib/Crypto/PublicKey/RSA.py:700:58 error[invalid-argument-type] Argument to bound method `IntegerBase.inverse` is incorrect: Expected `IntegerBase | int`, found `None`

scrapy (https://github.com/scrapy/scrapy)
- tests/test_http_response.py:328:35 error[invalid-argument-type] Argument to bound method `Response.follow_all` is incorrect: Expected `Iterable[str | Link]`, found `None`
- tests/test_http_response.py:330:35 error[invalid-argument-type] Argument to bound method `Response.follow_all` is incorrect: Expected `Iterable[str | Link]`, found `Literal[12345]`
- tests/test_http_response.py:332:35 error[invalid-argument-type] Argument to bound method `Response.follow_all` is incorrect: Expected `Iterable[str | Link]`, found `list[None]`

trio (https://github.com/python-trio/trio)
- src/trio/_core/_tests/test_run.py:2440:24 error[invalid-assignment] Object of type `Value[object] | Error` is not assignable to `Outcome[str] | None`
- src/trio/_core/_tests/test_run.py:2457:34 error[invalid-argument-type] Argument to bound method `CoroutineType.send` is incorrect: Expected `Outcome[object]`, found `None`
- src/trio/_core/_tests/test_run.py:2470:24 error[invalid-argument-type] Argument to bound method `CoroutineType.send` is incorrect: Expected `Outcome[object]`, found `None`

werkzeug (https://github.com/pallets/werkzeug)
- tests/test_test.py:150:9 error[invalid-assignment] Invalid subscript assignment with key of type `Literal["test_int"]` and value of type `Literal[1]` on object of type `MultiDict[str, str]`

xarray (https://github.com/pydata/xarray)
- xarray/tests/test_cftime_offsets.py:326:22 warning[possibly-unresolved-reference] Name `expected` used when possibly not defined

Full report with detailed diff (timing results)

@charliermarsh charliermarsh marked this pull request as ready for review June 24, 2026 00:18
@charliermarsh charliermarsh requested a review from a team as a code owner June 24, 2026 00:18
@astral-sh-bot astral-sh-bot Bot requested a review from sharkdp June 24, 2026 00:18
@codspeed-hq

codspeed-hq Bot commented Jun 24, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 20.19%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 1 regressed benchmark
✅ 70 untouched benchmarks
⏩ 64 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation ty_micro[literal_equality_fallthrough_guarded_any] 131.6 ms 164.9 ms -20.19%

Tip

Investigate this regression with the CodSpeed MCP and your agent.


Comparing charlie/infer-equality-comparison-results (963c3e3) with main (bcd2028)

Open in CodSpeed

Footnotes

  1. 64 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.

@charliermarsh charliermarsh merged commit a30ba16 into main Jun 24, 2026
62 of 63 checks passed
@charliermarsh charliermarsh deleted the charlie/infer-equality-comparison-results branch June 24, 2026 00:29
@charliermarsh

Copy link
Copy Markdown
Member Author

Ugh, I merged the wrong PR. Undoing, then re-opening.

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

Labels

internal An internal refactor or improvement ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant