Skip to content

fix Narrowing for isinstance and type[T] is unsound #713#2284

Closed
asukaminato0721 wants to merge 2 commits intofacebook:mainfrom
asukaminato0721:713
Closed

fix Narrowing for isinstance and type[T] is unsound #713#2284
asukaminato0721 wants to merge 2 commits intofacebook:mainfrom
asukaminato0721:713

Conversation

@asukaminato0721
Copy link
Copy Markdown
Contributor

@asukaminato0721 asukaminato0721 commented Feb 1, 2026

Summary

Fixes #713

Adjusted isinstance negative narrowing so we only subtract when the classinfo is a concrete class object (a class def or alias to one), avoiding the unsound narrowing for type[T].

Adjusted negative isinstance narrowing so we only treat type[...] classinfo as concrete when the classinfo expression is a precise literal (class defs combined with | or tuple literals).

Test Plan

Added a regression test that mirrors the issue case to ensure the else branch stays at int | str.

Copilot AI review requested due to automatic review settings February 1, 2026 13:36
@meta-cla meta-cla Bot added the cla signed label Feb 1, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes unsound negative narrowing for isinstance(x, cls) when cls has type type[T] by only performing negative subtraction when the classinfo is a concrete class object.

Changes:

  • Restrict negative isinstance narrowing to only subtract when classinfo is a concrete ClassDef (or an alias to one).
  • Add helper methods to detect concrete classinfo for negative narrowing.
  • Add a regression test ensuring the else branch does not narrow for cls: type[int].

Reviewed changes

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

File Description
pyrefly/lib/alt/narrow.rs Adjusts negative isinstance narrowing to avoid subtracting for non-concrete classinfo like type[T].
pyrefly/lib/test/narrow.rs Adds regression coverage to ensure type[T] does not trigger unsound negative narrowing.

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

@github-actions

This comment has been minimized.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 1, 2026

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

pytest-robotframework (https://github.com/detachhead/pytest-robotframework)
- ERROR pytest_robotframework/__init__.py:136:35-37: `TracebackType | None` is not assignable to attribute `__traceback__` with type `Never` [bad-assignment]
- ::error file=pytest_robotframework/__init__.py,line=136,col=35,endLine=136,endColumn=37,title=Pyrefly bad-assignment::`TracebackType | None` is not assignable to attribute `__traceback__` with type `Never`

kopf (https://github.com/nolar/kopf)
- ERROR kopf/_cogs/structs/dicts.py:259:14-38: Runtime checkable protocol `Iterable` has an unsafe overlap with type `Never` [unsafe-overlap]
- ::error file=kopf/_cogs/structs/dicts.py,line=259,col=14,endLine=259,endColumn=38,title=Pyrefly unsafe-overlap::Runtime checkable protocol `Iterable` has an unsafe overlap with type `Never`%0A  Attribute `__iter__` has incompatible types: `Never.__iter__` has type `Never`, which is not consistent with `BoundMethod[Never, (self: Never) -> Iterator[@_]]` in `Iterable.__iter__` (the type of read-write attributes cannot be changed)

porcupine (https://github.com/Akuli/porcupine)
- ERROR porcupine/plugins/highlight/pygments_highlighter.py:51:12-52: Object of class `type` has no attribute `get_tokens_unprocessed` [missing-attribute]
- ::error file=porcupine/plugins/highlight/pygments_highlighter.py,line=51,col=12,endLine=51,endColumn=52,title=Pyrefly missing-attribute::Object of class `type` has no attribute `get_tokens_unprocessed`

ibis (https://github.com/ibis-project/ibis)
- ::error file=ibis/expr/operations/window.py,line=50,col=9,endLine=50,endColumn=19,title=Pyrefly bad-override::Class member `WindowBoundary.__coerce__` overrides parent class `Value` in an inconsistent manner%0A  `WindowBoundary.__coerce__` has type `BoundMethod[type[WindowBoundary[T, S]], (cls: type[WindowBoundary[T, S]], value: Unknown, **kwargs: Unknown) -> WindowBoundary[T, S] | WindowBoundary[Unknown, Any]]`, which is not assignable to `BoundMethod[type[WindowBoundary[T, S]], (cls: type[WindowBoundary[T, S]], value: Any, T: type[Any] | None = None, S: type[Any] | None = None) -> WindowBoundary[T, S]]`, the type of `Value.__coerce__`
+ ::error file=ibis/expr/operations/window.py,line=50,col=9,endLine=50,endColumn=19,title=Pyrefly bad-override::Class member `WindowBoundary.__coerce__` overrides parent class `Value` in an inconsistent manner%0A  `WindowBoundary.__coerce__` has type `BoundMethod[type[WindowBoundary[T, S]], (cls: type[WindowBoundary[T, S]], value: Unknown, **kwargs: Unknown) -> WindowBoundary[Interval | Numeric, Any] | WindowBoundary[T, S] | WindowBoundary[Unknown, Scalar] | WindowBoundary[Unknown, Any]]`, which is not assignable to `BoundMethod[type[WindowBoundary[T, S]], (cls: type[WindowBoundary[T, S]], value: Any, T: type[Any] | None = None, S: type[Any] | None = None) -> WindowBoundary[T, S]]`, the type of `Value.__coerce__`

xarray (https://github.com/pydata/xarray)
+ ERROR xarray/core/dataarray.py:1491:51-57: Argument `Mapping[Any, T_ChunkDimFreq] | tuple[int, ...] | None` is not assignable to parameter `pos_kwargs` with type `Mapping[Any, T_ChunkDimFreq] | None` in function `xarray.namedarray.utils.either_dict_or_kwargs` [bad-argument-type]
+ ERROR xarray/namedarray/core.py:818:44-50: Argument `Mapping[Any, T_ChunkDim] | dict[Any, T_ChunkDim] | tuple[int, ...]` is not assignable to parameter `pos_kwargs` with type `Mapping[Any, T_ChunkDim] | None` in function `xarray.namedarray.utils.either_dict_or_kwargs` [bad-argument-type]
+ ::error file=xarray/core/dataarray.py,line=1491,col=51,endLine=1491,endColumn=57,title=Pyrefly bad-argument-type::Argument `Mapping[Any, T_ChunkDimFreq] | tuple[int, ...] | None` is not assignable to parameter `pos_kwargs` with type `Mapping[Any, T_ChunkDimFreq] | None` in function `xarray.namedarray.utils.either_dict_or_kwargs`
+ ::error file=xarray/namedarray/core.py,line=818,col=44,endLine=818,endColumn=50,title=Pyrefly bad-argument-type::Argument `Mapping[Any, T_ChunkDim] | dict[Any, T_ChunkDim] | tuple[int, ...]` is not assignable to parameter `pos_kwargs` with type `Mapping[Any, T_ChunkDim] | None` in function `xarray.namedarray.utils.either_dict_or_kwargs`

strawberry (https://github.com/strawberry-graphql/strawberry)
+ ERROR strawberry/http/__init__.py:28:26-39: Object of class `NoneType` has no attribute `errors` [missing-attribute]
+ ERROR strawberry/http/__init__.py:28:41-58: Object of class `NoneType` has no attribute `extensions` [missing-attribute]
+ ERROR strawberry/http/__init__.py:30:17-28: Object of class `NoneType` has no attribute `data` [missing-attribute]
+ ERROR strawberry/schema/schema.py:541:12-25: Object of class `NoneType` has no attribute `errors` [missing-attribute]
+ ERROR strawberry/schema/schema.py:542:44-57: `list[GraphQLError] | object | Unknown` is not assignable to attribute `pre_execution_errors` with type `list[GraphQLError] | None` [bad-assignment]
+ ERROR strawberry/schema/schema.py:544:38-51: Argument `list[GraphQLError] | object | Unknown` is not assignable to parameter `errors` with type `list[GraphQLError]` in function `strawberry.schema.base.BaseSchema._process_errors` [bad-argument-type]
+ ERROR strawberry/schema/schema.py:546:63-76: Argument `list[GraphQLError] | object | Unknown | None` is not assignable to parameter `errors` with type `list[GraphQLError] | None` in function `strawberry.types.execution.ExecutionResult.__init__` [bad-argument-type]
+ ERROR strawberry/schema/schema.py:547:9-26: Object of class `NoneType` has no attribute `extensions` [missing-attribute]
+ ERROR strawberry/schema/schema.py:549:16-22: Returned type `ExecutionResult | NoneType | Unknown` is not assignable to declared return type `ExecutionResult` [bad-return]
+ ::error file=strawberry/http/__init__.py,line=28,col=26,endLine=28,endColumn=39,title=Pyrefly missing-attribute::Object of class `NoneType` has no attribute `errors`
+ ::error file=strawberry/http/__init__.py,line=28,col=41,endLine=28,endColumn=58,title=Pyrefly missing-attribute::Object of class `NoneType` has no attribute `extensions`
+ ::error file=strawberry/http/__init__.py,line=30,col=17,endLine=30,endColumn=28,title=Pyrefly missing-attribute::Object of class `NoneType` has no attribute `data`
+ ::error file=strawberry/schema/schema.py,line=541,col=12,endLine=541,endColumn=25,title=Pyrefly missing-attribute::Object of class `NoneType` has no attribute `errors`
+ ::error file=strawberry/schema/schema.py,line=542,col=44,endLine=542,endColumn=57,title=Pyrefly bad-assignment::`list[GraphQLError] | object | Unknown` is not assignable to attribute `pre_execution_errors` with type `list[GraphQLError] | None`
+ ::error file=strawberry/schema/schema.py,line=544,col=38,endLine=544,endColumn=51,title=Pyrefly bad-argument-type::Argument `list[GraphQLError] | object | Unknown` is not assignable to parameter `errors` with type `list[GraphQLError]` in function `strawberry.schema.base.BaseSchema._process_errors`
+ ::error file=strawberry/schema/schema.py,line=546,col=63,endLine=546,endColumn=76,title=Pyrefly bad-argument-type::Argument `list[GraphQLError] | object | Unknown | None` is not assignable to parameter `errors` with type `list[GraphQLError] | None` in function `strawberry.types.execution.ExecutionResult.__init__`
+ ::error file=strawberry/schema/schema.py,line=547,col=9,endLine=547,endColumn=26,title=Pyrefly missing-attribute::Object of class `NoneType` has no attribute `extensions`
+ ::error file=strawberry/schema/schema.py,line=549,col=16,endLine=549,endColumn=22,title=Pyrefly bad-return::Returned type `ExecutionResult | NoneType | Unknown` is not assignable to declared return type `ExecutionResult`

pandera (https://github.com/pandera-dev/pandera)
+ ERROR pandera/backends/ibis/base.py:168:17-36: Object of class `NoneType` has no attribute `rename` [missing-attribute]
+ ERROR pandera/backends/polars/base.py:57:34-50: Object of class `object` has no attribute `sample` [missing-attribute]
+ ERROR pandera/backends/polars/base.py:178:24-49: Object of class `NoneType` has no attribute `columns` [missing-attribute]
+ ERROR pandera/backends/polars/base.py:181:40-70: Object of class `NoneType` has no attribute `with_columns` [missing-attribute]
+ ERROR pandera/backends/polars/base.py:183:29-51: Object of class `NoneType` has no attribute `rows` [missing-attribute]
+ ERROR pandera/backends/polars/base.py:187:40-64: Object of class `NoneType` has no attribute `rename` [missing-attribute]
+ ERROR pandera/backends/polars/base.py:188:26-54: Cannot index into `object` [bad-index]
+ ::error file=pandera/backends/ibis/base.py,line=168,col=17,endLine=168,endColumn=36,title=Pyrefly missing-attribute::Object of class `NoneType` has no attribute `rename`
+ ::error file=pandera/backends/polars/base.py,line=57,col=34,endLine=57,endColumn=50,title=Pyrefly missing-attribute::Object of class `object` has no attribute `sample`
+ ::error file=pandera/backends/polars/base.py,line=178,col=24,endLine=178,endColumn=49,title=Pyrefly missing-attribute::Object of class `NoneType` has no attribute `columns`
+ ::error file=pandera/backends/polars/base.py,line=181,col=40,endLine=181,endColumn=70,title=Pyrefly missing-attribute::Object of class `NoneType` has no attribute `with_columns`
+ ::error file=pandera/backends/polars/base.py,line=183,col=29,endLine=183,endColumn=51,title=Pyrefly missing-attribute::Object of class `NoneType` has no attribute `rows`
+ ::error file=pandera/backends/polars/base.py,line=187,col=40,endLine=187,endColumn=64,title=Pyrefly missing-attribute::Object of class `NoneType` has no attribute `rename`
+ ::error file=pandera/backends/polars/base.py,line=188,col=26,endLine=188,endColumn=54,title=Pyrefly bad-index::Cannot index into `object`%0A  Object of class `object` has no attribute `__getitem__`

websockets (https://github.com/aaugustin/websockets)
+ ERROR src/websockets/sync/connection.py:497:34-42: Runtime checkable protocol `Iterable` has an unsafe overlap with type `Iterable[DataLike] | bytearray | bytes | memoryview[int]` [unsafe-overlap]
+ ::error file=src/websockets/sync/connection.py,line=497,col=34,endLine=497,endColumn=42,title=Pyrefly unsafe-overlap::Runtime checkable protocol `Iterable` has an unsafe overlap with type `Iterable[DataLike] | bytearray | bytes | memoryview[int]`%0A  Attribute `__iter__` has incompatible types: `Iterable[DataLike] | bytearray | bytes | memoryview[int].__iter__` has type `BoundMethod[bytearray, (self: bytearray) -> Iterator[int]]`, which is not assignable to `BoundMethod[Iterable[DataLike] | bytearray | bytes | memoryview[int], (self: Iterable[DataLike] | bytearray | bytes | memoryview[int]) -> Iterator[@_]]`, the type of `Iterable.__iter__`

pylint (https://github.com/pycqa/pylint)
+ ERROR pylint/checkers/async_checker.py:74:25-41: Object of class `NoneType` has no attribute `getattr` [missing-attribute]
+ ERROR pylint/checkers/async_checker.py:75:25-41: Object of class `NoneType` has no attribute `getattr` [missing-attribute]
+ ERROR pylint/checkers/typecheck.py:1940:25-41: Object of class `NoneType` has no attribute `getattr` [missing-attribute]
+ ERROR pylint/checkers/typecheck.py:1941:25-41: Object of class `NoneType` has no attribute `getattr` [missing-attribute]
+ ERROR pylint/checkers/typecheck.py:1957:69-82: Object of class `NoneType` has no attribute `name` [missing-attribute]
+ ::error file=pylint/checkers/async_checker.py,line=74,col=25,endLine=74,endColumn=41,title=Pyrefly missing-attribute::Object of class `NoneType` has no attribute `getattr`
+ ::error file=pylint/checkers/async_checker.py,line=75,col=25,endLine=75,endColumn=41,title=Pyrefly missing-attribute::Object of class `NoneType` has no attribute `getattr`
+ ::error file=pylint/checkers/typecheck.py,line=1940,col=25,endLine=1940,endColumn=41,title=Pyrefly missing-attribute::Object of class `NoneType` has no attribute `getattr`
+ ::error file=pylint/checkers/typecheck.py,line=1941,col=25,endLine=1941,endColumn=41,title=Pyrefly missing-attribute::Object of class `NoneType` has no attribute `getattr`
+ ::error file=pylint/checkers/typecheck.py,line=1957,col=69,endLine=1957,endColumn=82,title=Pyrefly missing-attribute::Object of class `NoneType` has no attribute `name`

stone (https://github.com/dropbox/stone)
- ERROR stone/backends/python_rsrc/stone_base.py:105:75-90: Expected class object, got `Never` [invalid-argument]
- ERROR stone/backends/python_rsrc/stone_base.py:155:69-83: Expected class object, got `Never` [invalid-argument]
- ::error file=stone/backends/python_rsrc/stone_base.py,line=105,col=75,endLine=105,endColumn=90,title=Pyrefly invalid-argument::Expected class object, got `Never`
- ::error file=stone/backends/python_rsrc/stone_base.py,line=155,col=69,endLine=155,endColumn=83,title=Pyrefly invalid-argument::Expected class object, got `Never`

core (https://github.com/home-assistant/core)
+ ERROR homeassistant/components/deconz/entity.py:141:39-61: Object of class `object` has no attribute `reachable` [missing-attribute]
+ ERROR homeassistant/components/litterrobot/entity.py:57:61-69: Object of class `object` has no attribute `id` [missing-attribute]
+ ERROR homeassistant/components/shelly/coordinator.py:246:62-82: Object of class `object` has no attribute `settings` [missing-attribute]
- ERROR homeassistant/components/ssdp/scanner.py:538:23-37: Cannot set item in `dict[Never, Never]` [unsupported-operation]
- ERROR homeassistant/components/ssdp/scanner.py:538:41-44: Cannot set item in `dict[Never, Never]` [unsupported-operation]
- ERROR homeassistant/components/ssdp/scanner.py:549:14-23: Argument `dict[Unknown, Unknown] | dict[Never, Never]` is not assignable to parameter `upnp` with type `Mapping[str, Any]` in function `homeassistant.helpers.service_info.ssdp.SsdpServiceInfo.__init__` [bad-argument-type]
+ ::error file=homeassistant/components/deconz/entity.py,line=141,col=39,endLine=141,endColumn=61,title=Pyrefly missing-attribute::Object of class `object` has no attribute `reachable`
+ ::error file=homeassistant/components/litterrobot/entity.py,line=57,col=61,endLine=57,endColumn=69,title=Pyrefly missing-attribute::Object of class `object` has no attribute `id`
+ ::error file=homeassistant/components/shelly/coordinator.py,line=246,col=62,endLine=246,endColumn=82,title=Pyrefly missing-attribute::Object of class `object` has no attribute `settings`
- ::error file=homeassistant/components/ssdp/scanner.py,line=538,col=23,endLine=538,endColumn=37,title=Pyrefly unsupported-operation::Cannot set item in `dict[Never, Never]`%0A  Argument `Literal['UDN']` is not assignable to parameter `key` with type `Never` in function `dict.__setitem__`
- ::error file=homeassistant/components/ssdp/scanner.py,line=538,col=41,endLine=538,endColumn=44,title=Pyrefly unsupported-operation::Cannot set item in `dict[Never, Never]`%0A  Argument `str` is not assignable to parameter `value` with type `Never` in function `dict.__setitem__`
- ::error file=homeassistant/components/ssdp/scanner.py,line=549,col=14,endLine=549,endColumn=23,title=Pyrefly bad-argument-type::Argument `dict[Unknown, Unknown] | dict[Never, Never]` is not assignable to parameter `upnp` with type `Mapping[str, Any]` in function `homeassistant.helpers.service_info.ssdp.SsdpServiceInfo.__init__`

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


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

Comment thread pyrefly/lib/alt/narrow.rs
Comment on lines +302 to +307
let allow_unwrap = matches!(right, Type::ClassDef(_))
|| (allow_type_form_classinfo
&& matches!(&right, Type::Type(box Type::ClassType(_))));
if allow_unwrap
&& let Some((tparams, right)) = self.unwrap_class_object_silently(&right)
{
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

matches!(right, Type::ClassDef(_)) pattern-matches by value and will move right, but right is then borrowed again in unwrap_class_object_silently(&right), which will not compile. Use a borrowed match (e.g., match on &right) so right isn't moved when computing allow_unwrap.

Copilot uses AI. Check for mistakes.
@connernilsen connernilsen requested review from migeed-z and removed request for yangdanny97 February 4, 2026 20:21
@rchen152 rchen152 assigned rchen152 and unassigned migeed-z Feb 5, 2026
@rchen152
Copy link
Copy Markdown
Contributor

rchen152 commented Feb 5, 2026

I'll take this one, since I've looked at a couple similar narrowing PRs recently.

@meta-codesync
Copy link
Copy Markdown
Contributor

meta-codesync Bot commented Feb 6, 2026

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

Copy link
Copy Markdown
Contributor

@stroxler stroxler left a comment

Choose a reason for hiding this comment

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

Review automatically exported from Phabricator review in Meta.

@meta-codesync
Copy link
Copy Markdown
Contributor

meta-codesync Bot commented Feb 6, 2026

@rchen152 merged this pull request in abe13f0.

@asukaminato0721 asukaminato0721 deleted the 713 branch February 7, 2026 00:07
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.

Narrowing for isinstance and type[T] is unsound

8 participants