Skip to content

[ty] Improve equality-based narrowing#25788

Draft
charliermarsh wants to merge 8 commits into
mainfrom
charlie/revive-equality-narrowing
Draft

[ty] Improve equality-based narrowing#25788
charliermarsh wants to merge 8 commits into
mainfrom
charlie/revive-equality-narrowing

Conversation

@charliermarsh

Copy link
Copy Markdown
Member

Summary

This revives the equality-narrowing work from #22799 and gives == and != separate evaluators. Python can define __eq__ and __ne__ independently, so deriving one operator from the other can make reachable branches appear impossible.

The evaluator handles the comparison domains where ty knows the runtime semantics, including primitive literals, enums that inherit builtin comparison methods, and identity-based singleton values. It remains conservative for arbitrary user-defined comparison methods, preserving the existing behavior on main where the comparison result cannot be proven.

This improves narrowing for comparisons such as:

def f(value: bool | None, other: bool) -> None:
    if value == other:
        reveal_type(value)  # revealed: bool

Value-pattern reachability now uses the same equality semantics instead of type equivalence. This recognizes that a StrEnum member and a string literal with the same value compare equal in a match statement.

Closes astral-sh/ty#2732.
Closes astral-sh/ty#1454.

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

astral-sh-bot Bot commented Jun 9, 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 92.23%. The percentage of expected errors that received a diagnostic held steady at 87.42%. The number of fully passing files held steady at 92/134.

@astral-sh-bot

astral-sh-bot Bot commented Jun 9, 2026

Copy link
Copy Markdown

Memory usage report

Summary

Project Old New Diff Outcome
flake8 35.38MB 35.38MB +0.00% (1.22kB)
trio 87.75MB 87.68MB -0.08% (74.46kB) ⬇️
sphinx 207.56MB 207.41MB -0.07% (149.67kB) ⬇️
prefect 564.13MB 563.24MB -0.16% (914.66kB) ⬇️

Significant changes

Click to expand detailed breakdown

flake8

Name Old New Diff Outcome
all_narrowing_constraints_for_expression 167.11kB 157.86kB -5.53% (9.25kB)
infer_definition_types 1.57MB 1.58MB +0.12% (1.96kB)
Type<'db>::class_member_with_policy_ 316.53kB 318.28kB +0.55% (1.75kB)
member_lookup_with_policy_inner 404.51kB 406.19kB +0.42% (1.68kB)
member_lookup_with_policy_inner::interned_arguments 201.80kB 202.73kB +0.46% (960.00B)
Type<'db>::class_member_with_policy_::interned_arguments 163.41kB 164.23kB +0.50% (832.00B)
infer_expression_types_impl 974.41kB 975.01kB +0.06% (612.00B)
loop_header_reachability 12.41kB 12.97kB +4.53% (576.00B)
IntersectionType 51.73kB 52.17kB +0.85% (448.00B)
IntersectionType<'db>::from_two_elements_ 5.46kB 5.84kB +6.93% (388.00B)
UnionType 68.27kB 68.48kB +0.32% (224.00B)
IntersectionType<'db>::from_two_elements_::interned_arguments 5.84kB 6.02kB +2.94% (176.00B)
TupleType<'db>::to_class_type_ 13.89kB 14.03kB +0.98% (140.00B)
Type<'db>::apply_specialization_inner_ 181.69kB 181.80kB +0.06% (116.00B)
OverloadLiteral 120.72kB 120.83kB +0.09% (112.00B)
... 12 more

trio

Name Old New Diff Outcome
all_narrowing_constraints_for_expression 1012.84kB 963.37kB -4.88% (49.47kB) ⬇️
Type<'db>::class_member_with_policy_ 1.15MB 1.14MB -0.92% (10.83kB) ⬇️
member_lookup_with_policy_inner::interned_arguments 767.46kB 761.72kB -0.75% (5.74kB) ⬇️
Type<'db>::class_member_with_policy_::interned_arguments 616.59kB 611.61kB -0.81% (4.98kB) ⬇️
IntersectionType 144.58kB 141.10kB -2.40% (3.48kB) ⬇️
EnumComplementType 3.48kB 512.00B -85.65% (2.98kB) ⬇️
infer_expression_types_impl 6.57MB 6.58MB +0.04% (2.43kB) ⬇️
member_lookup_with_policy_inner 1.42MB 1.41MB -0.11% (1.66kB) ⬇️
StaticClassLiteral<'db>::try_mro_ 715.82kB 714.20kB -0.23% (1.62kB) ⬇️
is_redundant_with_impl::interned_arguments 224.73kB 225.76kB +0.46% (1.03kB) ⬇️
UnionType 151.14kB 150.56kB -0.38% (592.00B) ⬇️
IntersectionType<'db>::from_two_elements_ 16.33kB 16.83kB +3.06% (512.00B) ⬇️
place_table 78.63kB 79.10kB +0.60% (480.00B) ⬇️
FunctionType<'db>::signature_ 746.04kB 746.50kB +0.06% (468.00B) ⬇️
Specialization 454.97kB 454.53kB -0.10% (448.00B) ⬇️
... 26 more

sphinx

Name Old New Diff Outcome
all_narrowing_constraints_for_expression 4.12MB 3.92MB -4.74% (199.84kB) ⬇️
UnionType 654.20kB 660.12kB +0.91% (5.92kB) ⬇️
infer_expression_types_impl 20.49MB 20.49MB +0.03% (5.79kB) ⬇️
UnionType<'db>::from_two_elements_ 292.57kB 297.95kB +1.84% (5.38kB) ⬇️
is_redundant_with_impl 919.62kB 924.69kB +0.55% (5.07kB) ⬇️
member_lookup_with_policy_inner 5.64MB 5.64MB +0.08% (4.70kB) ⬇️
TupleType<'db>::to_class_type_ 159.64kB 164.30kB +2.92% (4.66kB) ⬇️
is_redundant_with_impl::interned_arguments 1.14MB 1.15MB +0.35% (4.12kB) ⬇️
StaticClassLiteral<'db>::try_mro_ 2.23MB 2.23MB -0.14% (3.24kB) ⬇️
Type<'db>::class_member_with_policy_ 4.63MB 4.63MB +0.06% (2.61kB) ⬇️
Specialization 1.27MB 1.27MB +0.19% (2.52kB) ⬇️
infer_definition_types 20.72MB 20.73MB +0.01% (2.50kB) ⬇️
infer_scope_types_impl 12.55MB 12.55MB +0.02% (2.19kB) ⬇️
Type<'db>::apply_specialization_inner_ 1.52MB 1.52MB +0.13% (2.01kB) ⬇️
GenericAlias 546.89kB 548.51kB +0.30% (1.62kB) ⬇️
... 21 more

prefect

Name Old New Diff Outcome
all_narrowing_constraints_for_expression 13.21MB 12.27MB -7.11% (962.47kB) ⬇️
infer_definition_types 75.87MB 75.88MB +0.01% (9.66kB) ⬇️
member_lookup_with_policy_inner 13.53MB 13.54MB +0.06% (8.59kB) ⬇️
infer_expression_types_impl 55.21MB 55.22MB +0.01% (7.77kB) ⬇️
is_redundant_with_impl::interned_arguments 2.37MB 2.38MB +0.30% (7.30kB) ⬇️
is_redundant_with_impl 1.99MB 1.99MB +0.28% (5.60kB) ⬇️
IntersectionType<'db>::from_two_elements_ 87.81kB 89.90kB +2.38% (2.09kB) ⬇️
StaticClassLiteral<'db>::try_mro_ 4.33MB 4.33MB -0.04% (1.62kB) ⬇️
infer_scope_types_impl 47.64MB 47.65MB +0.00% (1.58kB) ⬇️
TupleType<'db>::to_class_type_ 469.35kB 470.93kB +0.34% (1.58kB) ⬇️
Type<'db>::apply_specialization_inner_ 3.18MB 3.18MB +0.05% (1.54kB) ⬇️
UnionType 1.37MB 1.38MB +0.11% (1.50kB) ⬇️
member_lookup_with_policy_inner::interned_arguments 6.68MB 6.67MB -0.02% (1.41kB) ⬇️
Type<'db>::class_member_with_policy_::interned_arguments 5.67MB 5.67MB -0.02% (1.22kB) ⬇️
place_by_id 5.04MB 5.04MB +0.02% (1.05kB) ⬇️
... 16 more

@codspeed-hq

codspeed-hq Bot commented Jun 9, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 67 untouched benchmarks
⏩ 60 skipped benchmarks1


Comparing charlie/revive-equality-narrowing (35f160f) with main (e201fae)

Open in CodSpeed

Footnotes

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

@astral-sh-bot

astral-sh-bot Bot commented Jun 9, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

Lint rule Added Removed Changed
unsupported-operator 0 0 12
invalid-argument-type 0 1 8
type-assertion-failure 0 0 1
Total 0 1 21
Raw diff (22 changes)
core (https://github.com/home-assistant/core)
- homeassistant/components/lutron/light.py:91:50 error[invalid-argument-type] Argument to function `to_lutron_level` is incorrect: Expected `int`, found `Any | (int & ~Literal[0]) | None`
+ homeassistant/components/lutron/light.py:91:50 error[invalid-argument-type] Argument to function `to_lutron_level` is incorrect: Expected `int`, found `Any | (int & ~Literal[0] & ~Literal[False]) | None`

hydpy (https://github.com/hydpy-dev/hydpy)
- hydpy/core/modeltools.py:3163:25 error[type-assertion-failure] Type `Any & ~Literal[0] & ~Literal[1]` is not equivalent to `Never`
+ hydpy/core/modeltools.py:3163:25 error[type-assertion-failure] Type `Any & ~Literal[0] & ~Literal[False] & ~Literal[1] & ~Literal[True]` is not equivalent to `Never`

paasta (https://github.com/yelp/paasta)
- paasta_tools/kubernetes_tools.py:951:13 error[invalid-argument-type] Argument to bound method `KubernetesDeploymentConfig.get_autoscaling_scaling_policy` is incorrect: Expected `int`, found `(int & ~Literal[0]) | None`
+ paasta_tools/kubernetes_tools.py:951:13 error[invalid-argument-type] Argument to bound method `KubernetesDeploymentConfig.get_autoscaling_scaling_policy` is incorrect: Expected `int`, found `(int & ~Literal[0] & ~Literal[False]) | None`

pandas (https://github.com/pandas-dev/pandas)
- pandas/core/arrays/datetimes.py:3233:28 error[unsupported-operator] Operator `-` is not supported between objects of type `(int & ~Literal[1]) | None` and `Literal[1]`
+ pandas/core/arrays/datetimes.py:3233:28 error[unsupported-operator] Operator `-` is not supported between objects of type `(int & ~Literal[1] & ~Literal[True]) | None` and `Literal[1]`
- pandas/core/arrays/datetimes.py:3242:28 error[unsupported-operator] Operator `-` is not supported between objects of type `(int & ~Literal[1]) | None` and `Literal[1]`
+ pandas/core/arrays/datetimes.py:3242:28 error[unsupported-operator] Operator `-` is not supported between objects of type `(int & ~Literal[1] & ~Literal[True]) | None` and `Literal[1]`
- pandas/core/frame.py:13859:42 error[invalid-argument-type] Argument to bound method `DataFrame.shift` is incorrect: Expected `int | Sequence[int]`, found `(int & ~Literal[0]) | integer[Any]`
+ pandas/core/frame.py:13859:42 error[invalid-argument-type] Argument to bound method `DataFrame.shift` is incorrect: Expected `int | Sequence[int]`, found `(int & ~Literal[0] & ~Literal[False]) | integer[Any]`
- pandas/core/reshape/tile.py:435:31 error[invalid-argument-type] Argument to function `abs` is incorrect: Expected `SupportsAbs[Unknown]`, found `~Literal[0]`
+ pandas/core/reshape/tile.py:435:31 error[invalid-argument-type] Argument to function `abs` is incorrect: Expected `SupportsAbs[Unknown]`, found `~Literal[0] & ~Literal[False]`
- pandas/core/reshape/tile.py:436:31 error[invalid-argument-type] Argument to function `abs` is incorrect: Expected `SupportsAbs[Unknown]`, found `~Literal[0]`
+ pandas/core/reshape/tile.py:436:31 error[invalid-argument-type] Argument to function `abs` is incorrect: Expected `SupportsAbs[Unknown]`, found `~Literal[0] & ~Literal[False]`

pwndbg (https://github.com/pwndbg/pwndbg)
- pwndbg/aglib/onegadget.py:366:104 error[invalid-argument-type] Argument to function `string` is incorrect: Expected `int`, found `(int & ~Literal[0]) | None`
+ pwndbg/aglib/onegadget.py:366:104 error[invalid-argument-type] Argument to function `string` is incorrect: Expected `int`, found `(int & ~Literal[0] & ~Literal[False]) | None`

scikit-learn (https://github.com/scikit-learn/scikit-learn)
- sklearn/preprocessing/_polynomial.py:533:31 error[unsupported-operator] Operator `+` is not supported between objects of type `~Literal[0]` and `Literal[1]`
+ sklearn/preprocessing/_polynomial.py:533:31 error[unsupported-operator] Operator `+` is not supported between objects of type `~Literal[0] & ~Literal[False]` and `Literal[1]`

scipy (https://github.com/scipy/scipy)
- scipy/interpolate/_polyint.py:109:33 error[unsupported-operator] Operator `+` is not supported between objects of type `int` and `(Unknown & ~Literal[0]) | None`
+ scipy/interpolate/_polyint.py:109:33 error[unsupported-operator] Operator `+` is not supported between objects of type `int` and `(Unknown & ~Literal[0] & ~Literal[False]) | None`
- scipy/interpolate/_polyint.py:110:49 error[unsupported-operator] Operator `+` is not supported between objects of type `int` and `(Unknown & ~Literal[0]) | None`
+ scipy/interpolate/_polyint.py:110:49 error[unsupported-operator] Operator `+` is not supported between objects of type `int` and `(Unknown & ~Literal[0] & ~Literal[False]) | None`
- scipy/interpolate/_polyint.py:193:41 error[unsupported-operator] Operator `+` is not supported between objects of type `int` and `(Unknown & ~Literal[0]) | None`
+ scipy/interpolate/_polyint.py:193:41 error[unsupported-operator] Operator `+` is not supported between objects of type `int` and `(Unknown & ~Literal[0] & ~Literal[False]) | None`
- scipy/interpolate/_polyint.py:195:29 error[unsupported-operator] Operator `+` is not supported between objects of type `int` and `(Unknown & ~Literal[0]) | None`
+ scipy/interpolate/_polyint.py:195:29 error[unsupported-operator] Operator `+` is not supported between objects of type `int` and `(Unknown & ~Literal[0] & ~Literal[False]) | None`
- scipy/stats/_distribution_infrastructure.py:1914:12 error[unsupported-operator] Operator `>` is not supported between objects of type `generic[Any] | (int & ~Literal[1]) | float | ... omitted 4 union elements` and `Literal[1]`
+ scipy/stats/_distribution_infrastructure.py:1914:12 error[unsupported-operator] Operator `>` is not supported between objects of type `generic[Any] | (int & ~Literal[1] & ~Literal[True]) | float | ... omitted 4 union elements` and `Literal[1]`
- subprojects/array_api_extra/src/array_api_extra/_lib/_funcs.py:614:34 error[invalid-argument-type] Argument to function `log2` is incorrect: Expected `SupportsFloat | SupportsIndex`, found `(int & ~Literal[0]) | None`
+ subprojects/array_api_extra/src/array_api_extra/_lib/_funcs.py:614:34 error[invalid-argument-type] Argument to function `log2` is incorrect: Expected `SupportsFloat | SupportsIndex`, found `(int & ~Literal[0] & ~Literal[False]) | None`

sympy (https://github.com/sympy/sympy)
- sympy/combinatorics/prufer.py:291:13 error[unsupported-operator] Operator `-=` is not supported between objects of type `None` and `None | (Unknown & ~Literal[0])`
+ sympy/combinatorics/prufer.py:291:13 error[unsupported-operator] Operator `-=` is not supported between objects of type `None` and `None | (Unknown & ~Literal[0] & ~Literal[False])`
- sympy/ntheory/tests/test_factor_.py:64:16 error[unsupported-operator] Operator `%` is not supported between objects of type `~AlwaysFalsy & ~Literal[1]` and `Literal[2]`
+ sympy/ntheory/tests/test_factor_.py:64:16 error[unsupported-operator] Operator `%` is not supported between objects of type `~AlwaysFalsy & ~Literal[1] & ~Literal[True]` and `Literal[2]`
- sympy/ntheory/tests/test_factor_.py:66:33 error[unsupported-operator] Operator `//` is not supported between objects of type `~AlwaysFalsy & ~Literal[1]` and `Literal[2]`
+ sympy/ntheory/tests/test_factor_.py:66:33 error[unsupported-operator] Operator `//` is not supported between objects of type `~AlwaysFalsy & ~Literal[1] & ~Literal[True]` and `Literal[2]`
- sympy/polys/polytools.py:4988:25 error[invalid-argument-type] Argument to bound method `Poly.degree` is incorrect: Expected `int`, found `Expr`
- sympy/solvers/diophantine/diophantine.py:3833:43 error[invalid-argument-type] Argument to function `_can_do_sum_of_squares` is incorrect: Expected `int`, found `(int & ~Literal[0]) | MPZ`
+ sympy/solvers/diophantine/diophantine.py:3833:43 error[invalid-argument-type] Argument to function `_can_do_sum_of_squares` is incorrect: Expected `int`, found `(int & ~Literal[0] & ~Literal[False]) | MPZ`

xarray (https://github.com/pydata/xarray)
- xarray/plot/utils.py:305:49 error[unsupported-operator] Operator `-` is not supported between objects of type `Hashable & ~Literal[1]` and `Literal[1]`
+ xarray/plot/utils.py:305:49 error[unsupported-operator] Operator `-` is not supported between objects of type `Hashable & ~Literal[1] & ~Literal[True]` and `Literal[1]`

Full report with detailed diff (timing results)

@charliermarsh charliermarsh force-pushed the charlie/revive-equality-narrowing branch from 35f160f to b0d6ec8 Compare June 10, 2026 13:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bool | None is not narrowed to bool after equaling a bool Incorrect narrowing of enums with custom __eq__ methods in match statements

1 participant