Skip to content

negative refinement for type(X) != Y, X.__class__ != Y when Y is final#2566

Closed
yangdanny97 wants to merge 2 commits intofacebook:mainfrom
yangdanny97:export-D94528111
Closed

negative refinement for type(X) != Y, X.__class__ != Y when Y is final#2566
yangdanny97 wants to merge 2 commits intofacebook:mainfrom
yangdanny97:export-D94528111

Conversation

@yangdanny97
Copy link
Contributor

Summary:
Previously we did not narrow in the negative case for type(X) and X.__class__, because it only checks whether something is exactly that type. However, if the RHS is a final class then we know there can be no subclasses, so it would be safe to narrow it away.

fixes #2530

Differential Revision: D94528111

Summary:
Fixes facebook#1163

Added a special-case facet narrow so `x.__class__ is/== T` narrows `x like isinstance(x, T)`.

Pull Request resolved: facebook#2475

Test Plan: Added coverage for `__class__` attribute narrowing with `assert/if` and `==`.

Differential Revision: D94521571

Pulled By: yangdanny97
@meta-codesync
Copy link

meta-codesync bot commented Feb 26, 2026

@yangdanny97 has exported this pull request. If you are a Meta employee, you can view the originating Diff in D94528111.

Copy link
Contributor

@rchen152 rchen152 left a comment

Choose a reason for hiding this comment

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

Review automatically exported from Phabricator review in Meta.

@github-actions

This comment has been minimized.

@yangdanny97 yangdanny97 self-assigned this Feb 26, 2026
facebook#2566)

Summary:
Pull Request resolved: facebook#2566

Previously we did not narrow in the negative case for `type(X)` and `X.__class__`, because it only checks whether something is _exactly_ that type. However,  if the RHS is a final class then we know there can be no subclasses, so it would be safe to narrow it away.

fixes facebook#2530

Reviewed By: rchen152

Differential Revision: D94528111
@github-actions
Copy link

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

pydantic (https://github.com/pydantic/pydantic)
- ERROR pydantic/networks.py:244:16-76: Returned type `Literal[False] | SupportsBool` is not assignable to declared return type `bool` [bad-return]
- ERROR pydantic/networks.py:247:16-76: Returned type `Literal[False] | SupportsBool` is not assignable to declared return type `bool` [bad-return]
- ERROR pydantic/networks.py:250:16-77: Returned type `Literal[False] | SupportsBool` is not assignable to declared return type `bool` [bad-return]
- ERROR pydantic/networks.py:253:16-77: Returned type `Literal[False] | SupportsBool` is not assignable to declared return type `bool` [bad-return]

werkzeug (https://github.com/pallets/werkzeug)
- ERROR tests/test_wrappers.py:372:16-28: Object of class `Response` has no attribute `foo` [missing-attribute]

static-frame (https://github.com/static-frame/static-frame)
- ERROR static_frame/core/frame.py:1307:31-32: Yielded type `Iterable[Any]` is not assignable to declared yield type `ndarray[Any, Any]` [invalid-yield]
- ERROR static_frame/core/frame.py:1444:31-32: Yielded type `Iterable[Any]` is not assignable to declared yield type `ndarray[Any, Any]` [invalid-yield]
- ERROR static_frame/core/frame.py:9295:24-41: Returned type `Self@Frame` is not assignable to declared return type `Frame[Any, Any, *tuple[Any, ...]]` [bad-return]
- ERROR static_frame/core/frame.py:9296:20-24: Returned type `Self@Frame` is not assignable to declared return type `Frame[Any, Any, *tuple[Any, ...]]` [bad-return]
- ERROR static_frame/core/index_hierarchy.py:1204:56-63: Argument `IndexHierarchy[*tuple[Any, ...]] | PendingRow | Sequence[TLabel]` is not assignable to parameter `iterable` with type `Iterable[Sequence[TLabel]]` in function `enumerate.__new__` [bad-argument-type]
- ERROR static_frame/core/type_blocks.py:1764:20-50: `>` is not supported between `int` and `object` [unsupported-operation]
- ERROR static_frame/core/type_blocks.py:3346:34-75: `Generator[Iterable[Any] | ndarray[Any, Any], None, None]` is not assignable to variable `other_operands` with type `Iterable[ndarray[Any, Any]]` [bad-assignment]
- ERROR static_frame/core/util.py:2099:26-2104:10: No matching overload found for function `numpy._core.multiarray.arange` called with arguments: (start=Unknown, stop=Unknown, step=Unknown, dtype=dtype[Any] | Unknown | None) [no-matching-overload]
+ ERROR static_frame/core/util.py:2099:26-2104:10: No matching overload found for function `numpy._core.multiarray.arange` called with arguments: (start=int, stop=int, step=int, dtype=dtype[Any] | Unknown | None) [no-matching-overload]
- ERROR static_frame/test/unit/test_metadata.py:126:26-36: Object of class `IndexBase` has no attribute `dtype` [missing-attribute]

jax (https://github.com/google/jax)
- ERROR jax/_src/interpreters/ad.py:157:14-24: Argument `Sequence[bool] | bool | tuple[bool, ...]` is not assignable to parameter `obj` with type `Sized` in function `len` [bad-argument-type]
- ERROR jax/_src/interpreters/ad.py:160:14-25: Argument `Sequence[bool] | bool | tuple[bool, ...]` is not assignable to parameter `obj` with type `Sized` in function `len` [bad-argument-type]
- ERROR jax/_src/interpreters/ad.py:161:57-68: Argument `Sequence[bool] | bool | tuple[bool, ...]` is not assignable to parameter `iterable` with type `Iterable[bool]` in function `tuple.__new__` [bad-argument-type]
- ERROR jax/_src/interpreters/ad.py:162:33-43: Argument `Sequence[bool] | bool | tuple[bool, ...]` is not assignable to parameter `iterable` with type `Iterable[bool]` in function `tuple.__new__` [bad-argument-type]
- ERROR jax/_src/interpreters/ad.py:1318:51-62: Argument `Sequence[bool] | bool | tuple[bool, ...]` is not assignable to parameter `iterable` with type `Iterable[bool]` in function `tuple.__new__` [bad-argument-type]

yangdanny97 added a commit to yangdanny97/pyrefly that referenced this pull request Feb 26, 2026
facebook#2566)

Summary:
Pull Request resolved: facebook#2566

Previously we did not narrow in the negative case for `type(X)` and `X.__class__`, because it only checks whether something is _exactly_ that type. However,  if the RHS is a final class then we know there can be no subclasses, so it would be safe to narrow it away.

fixes facebook#2530

Differential Revision: D94528111

Reviewed By: rchen152
@github-actions
Copy link

Primer Diff Classification

✅ 4 improvement(s) | 4 project(s) total

4 improvement(s) across pydantic, werkzeug, static-frame, jax.

Project Verdict Changes Error Kinds Root Cause
pydantic ✅ Improvement -4 bad-return narrow_type_not_eq()
werkzeug ✅ Improvement -1 missing-attribute narrow_type_not_eq()
static-frame ✅ Improvement +1, -9 bad-argument-type, bad-assignment narrow_type_not_eq()
jax ✅ Improvement -5 bad-argument-type narrow_type_not_eq()
Detailed analysis

✅ Improvement (4)

pydantic (-4)

These removed errors were false positives. The comparison methods __lt__, __gt__, __le__, __ge__ in the _BaseUrl class correctly return bool values. Each method uses the pattern return self.__class__ is other.__class__ and self._url < other._url (and similar for other operators). This expression evaluates to bool because both self.__class__ is other.__class__ and self._url < other._url return bool, and bool and bool produces bool. Pyrefly was incorrectly inferring the return type as Literal[False] | SupportsBool, which was wrong. The PR changes to type narrowing for __class__ comparisons fixed this inference issue, removing the false positive errors.
Attribution: The change to narrow_type_not_eq() and __class__ attribute narrowing in pyrefly/lib/alt/narrow.rs improved type narrowing for type() and __class__ comparisons. This likely fixed the type inference that was causing pyrefly to incorrectly infer Literal[False] | SupportsBool instead of the correct bool type for these comparison expressions.

werkzeug (-1)

This is an improvement. The error was a false positive - the code defines a SpecialResponse class with a foo() method, and force_type() explicitly converts the response to that type. Line 370 shows response = SpecialResponse.force_type(orig_resp, fake_env), which makes response have type SpecialResponse, not Response. Therefore, response.foo() on line 372 is valid since SpecialResponse defines the foo() method on line 362. The type checker was failing to properly track the type after the force_type() conversion, leading to an incorrect missing-attribute error.
Attribution: The change to narrow_type_not_eq() and related narrowing logic in pyrefly/lib/alt/narrow.rs improved type narrowing for type(X) != Y patterns and X.__class__ attribute access. This likely fixed the type inference for force_type() method calls, which perform type conversions that the narrowing system needs to track correctly.

static-frame (+1, -9)

This is an improvement. The new error appears to be pyrefly being overly strict about Unknown types in numpy.arange overload resolution - mypy/pyright would not flag this pattern. The removed errors were false positives where pyrefly's type inference was failing, incorrectly claiming type mismatches that don't actually exist. The PR improved type narrowing which resolved these inference failures, making pyrefly more accurate overall despite the one new overload resolution issue.
Attribution: The changes to narrow_type_not_eq() and narrow_facet_atomic() in pyrefly/lib/alt/narrow.rs improved type narrowing for type(x) != Y and x.__class__ == Y patterns. This better narrowing likely resolved many type inference issues that were causing the false positive errors in static_frame, but may have made overload resolution stricter, causing the new numpy.arange error.

jax (-5)

These removed errors were false positives. The union type Sequence[bool] | bool | tuple[bool, ...] should be assignable to Sized and Iterable[bool] because all union members satisfy these protocols: bool implements Sized (via __len__ returning 1), Sequence[bool] is Sized and Iterable[bool], and tuple[bool, ...] is also Sized and Iterable[bool]. The type checker was incorrectly rejecting these valid assignments. The PR improved type narrowing logic, which likely fixed the inference issues causing these false positives.
Attribution: The change to narrow_type_not_eq() and narrow_facet_op() in pyrefly/lib/alt/narrow.rs improved type narrowing for type(x) != Y and x.__class__ != Y patterns. This likely fixed the type inference that was causing the union types to be incorrectly analyzed, leading to the removal of these false positive errors.


Classification by primer-classifier (4 LLM) · Was this helpful? React with 👍 or 👎

yangdanny97 added a commit to yangdanny97/pyrefly that referenced this pull request Feb 26, 2026
facebook#2566)

Summary:
Pull Request resolved: facebook#2566

Previously we did not narrow in the negative case for `type(X)` and `X.__class__`, because it only checks whether something is _exactly_ that type. However,  if the RHS is a final class then we know there can be no subclasses, so it would be safe to narrow it away.

fixes facebook#2530

Differential Revision: D94528111

Reviewed By: rchen152
yangdanny97 added a commit to yangdanny97/pyrefly that referenced this pull request Feb 26, 2026
facebook#2566)

Summary:
Pull Request resolved: facebook#2566

Previously we did not narrow in the negative case for `type(X)` and `X.__class__`, because it only checks whether something is _exactly_ that type. However,  if the RHS is a final class then we know there can be no subclasses, so it would be safe to narrow it away.

fixes facebook#2530

Differential Revision: D94528111

Reviewed By: rchen152
@meta-codesync meta-codesync bot closed this in d984d74 Feb 26, 2026
@meta-codesync
Copy link

meta-codesync bot commented Feb 26, 2026

This pull request has been merged in d984d74.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No narrowing on type(...) is T

4 participants