Skip to content

[ty] Support narrowing on __class__ checks - #24997

Merged
charliermarsh merged 2 commits into
mainfrom
charlie/dunder-class-narrowing
May 8, 2026
Merged

charliermarsh merged 2 commits into
mainfrom
charlie/dunder-class-narrowing

Conversation

@charliermarsh

@charliermarsh charliermarsh commented May 5, 2026 •

Copy link
Copy Markdown
Member

Summary

This adds support for narrowing based on __class__ identity checks, treating them the same way we already treat type(...) checks. Although these can differ at runtime, AFAICT we already treat these as the same internally (i.e., infer_builtins_type_call calls arg_type.dunder_class(db)).

For example:

from typing import final, reveal_type

class A: ...
class B: ...

@final
class C: ...

def f(x: A | B, y: A | C):
    if x.__class__ is A:
        reveal_type(x)  # revealed: A

    if C is y.__class__:
        reveal_type(y)  # revealed: C
    else:
        reveal_type(y)  # revealed: A

This supports both operand orders, and both is and is not. As with type(x) is C, negative-branch narrowing is only applied when the tested class is final.

@charliermarsh charliermarsh added the ty The ty type checker label May 5, 2026
@astral-sh-bot

astral-sh-bot Bot commented May 5, 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 89.36%. The percentage of expected errors that received a diagnostic held steady at 85.49%. The number of fully passing files held steady at 88/134.

@astral-sh-bot

astral-sh-bot Bot commented May 5, 2026 •

Copy link
Copy Markdown

Memory usage report

Memory usage unchanged ✅

@astral-sh-bot

astral-sh-bot Bot commented May 5, 2026 •

Copy link
Copy Markdown

ecosystem-analyzer results

Failing projects:

Project Old Status New Status Old Return Code New Return Code
pywin32 abnormal exit abnormal exit 2 2

Diagnostic changes:

Lint rule Added Removed Changed
unresolved-attribute 0 57 0
unused-type-ignore-comment 14 9 0
invalid-argument-type 2 19 0
invalid-return-type 0 4 1
invalid-assignment 0 3 0
no-matching-overload 2 1 0
not-iterable 0 1 2
unsupported-operator 0 0 3
not-subscriptable 0 2 0
call-non-callable 0 1 0
invalid-declaration 0 0 1
Total 18 97 7
Raw diff (122 changes)
discord.py (https://github.com/Rapptz/discord.py)
+ discord/state.py:968:64 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive

ibis (https://github.com/ibis-project/ibis)
- ibis/common/patterns.py:936:13 error[unresolved-attribute] Object of type `Pattern` has no attribute `patterns`

pip (https://github.com/pypa/pip)
- src/pip/_internal/utils/misc.py:551:35 error[unresolved-attribute] Object of type `object` has no attribute `secret`

pydantic (https://github.com/pydantic/pydantic)
- pydantic/networks.py:244:16 error[invalid-return-type] Return type does not match returned value: expected `bool`, found `SupportsBool`
- pydantic/networks.py:247:16 error[invalid-return-type] Return type does not match returned value: expected `bool`, found `SupportsBool`
- pydantic/networks.py:250:16 error[invalid-return-type] Return type does not match returned value: expected `bool`, found `SupportsBool`
- pydantic/networks.py:253:16 error[invalid-return-type] Return type does not match returned value: expected `bool`, found `SupportsBool`

pywin32 (https://github.com/mhammond/pywin32)
- FAILED old=abnormal exit(2) new=abnormal exit(2)

spack (https://github.com/spack/spack)
- lib/spack/spack/vendor/jinja2/lexer.py:762:35 error[call-non-callable] Object of type `str` is not callable

static-frame (https://github.com/static-frame/static-frame)
- static_frame/core/container_util.py:1032:20 error[invalid-assignment] Object of type `(ndarray[Any, Any] & ~Frame) | None` is not assignable to `ndarray[Any, Any]`
- static_frame/core/container_util.py:1067:12 error[unresolved-attribute] Object of type `<Protocol with members '__len__'> & ~str` has no attribute `dtype`
- static_frame/core/container_util.py:1070:16 error[no-matching-overload] No overload of function `sort` matches arguments
- static_frame/core/container_util.py:1521:20 error[unresolved-attribute] Object of type `Iterable[Hashable] & ~IndexBase` has no attribute `ndim`
- static_frame/core/container_util.py:1522:73 error[unresolved-attribute] Object of type `Iterable[Hashable] & ~IndexBase` has no attribute `ndim`
- static_frame/core/container_util.py:1534:9 error[invalid-argument-type] Argument to `IMTOAdapter.__init__` is incorrect: Expected `ndarray[Any, Any]`, found `(Iterable[Hashable] & ~IndexBase) | ndarray[Any, Any]`
- static_frame/core/display.py:486:47 error[unresolved-attribute] Attribute `ndim` is not defined on `Sequence[Any]` in union `ndarray[Any, Any] | Sequence[Any]`
- static_frame/core/display.py:489:36 error[invalid-argument-type] Argument to function `array_str` is incorrect: Expected `ndarray[tuple[Any, ...], dtype[Any]]`, found `ndarray[Any, Any] | Sequence[Any]`
- static_frame/core/display.py:520:38 error[unresolved-attribute] Attribute `dtype` is not defined on `Sequence[Any]` in union `ndarray[Any, Any] | Sequence[Any]`
- static_frame/core/frame.py:7203:56 error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `int | integer[Any] | ndarray[Any, Any] | list[int] | slice[Any, Any, Any]`
+ static_frame/core/frame.py:5381:57 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ static_frame/core/frame.py:5392:64 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- static_frame/core/index.py:253:37 error[invalid-argument-type] Argument to function `immutable_filter` is incorrect: Expected `ndarray[tuple[Any, ...], dtype[Any]]`, found `ndarray[Any, Any] | None`
- static_frame/core/index.py:334:46 error[invalid-argument-type] Argument to function `immutable_filter` is incorrect: Expected `ndarray[tuple[Any, ...], dtype[Any]]`, found `Iterable[Hashable] & ~IndexBase`
- static_frame/core/index.py:460:18 error[unresolved-attribute] Attribute `dtype` is not defined on `Iterable[Hashable] & ~IndexBase & ~ContainerOperand`, `Iterator[Hashable]` in union `(Iterable[Hashable] & ~IndexBase & ~ContainerOperand) | Unknown | Iterator[Hashable] | ndarray[Any, Any]`
- static_frame/core/index.py:484:62 error[unresolved-attribute] Attribute `step` is not defined on `Iterable[Hashable] & ~IndexBase & ~ContainerOperand & ~str`, `Iterator[Hashable] & ~str`, `ndarray[Any, Any] & ~str` in union `(Iterable[Hashable] & ~IndexBase & ~ContainerOperand & ~str) | (Unknown & ~str) | (Iterator[Hashable] & ~str) | (ndarray[Any, Any] & ~str)`
- static_frame/core/index.py:691:46 error[unresolved-attribute] Attribute `dtype` is not defined on `int`, `list[int]`, `slice[Any, Any, Any]` in union `int | integer[Any] | ndarray[Any, Any] | list[int] | slice[Any, Any, Any]`
- static_frame/core/index.py:1119:74 error[invalid-argument-type] Argument to bound method `SortStatus.derive_status_from_slice` is incorrect: Expected `slice[Any, Any, Any]`, found `int | integer[Any] | ndarray[Any, Any] | list[int] | slice[Any, Any, Any]`
- static_frame/core/index.py:1946:25 error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `Iterable[Hashable] & ~IndexBase`
- static_frame/core/index.py:461:26 error[unresolved-attribute] Attribute `astype` is not defined on `Iterable[Hashable] & ~IndexBase & ~ContainerOperand`, `Iterator[Hashable]` in union `(Iterable[Hashable] & ~IndexBase & ~ContainerOperand) | Unknown | Iterator[Hashable] | ndarray[Any, Any]`
+ static_frame/core/index.py:461:26 error[no-matching-overload] No overload of bound method `ndarray.astype` matches arguments
- static_frame/core/index_hierarchy.py:2154:44 error[unresolved-attribute] Object of type `Hashable & ~int & ~Top[integer[Any]] & ~IndexHierarchy` has no attribute `dtype`
- static_frame/core/index_hierarchy.py:2155:20 error[invalid-argument-type] Method `__getitem__` of type `Overload[(key: ndarray[tuple[Any, ...], dtype[integer[Any] | numpy.bool[builtins.bool]]] | tuple[ndarray[tuple[Any, ...], dtype[integer[Any] | numpy.bool[builtins.bool]]], ...], /) -> ndarray[tuple[Any, ...], Any], (key: SupportsIndex | tuple[SupportsIndex, ...], /) -> Any, (key: SupportsIndex | slice[Any, Any, Any] | EllipsisType | ... omitted 5 union elements, /) -> ndarray[tuple[Any, ...], Any], (key: str, /) -> ndarray[Any | tuple[Any, ...], dtype[Any]], (key: list[str], /) -> ndarray[Any | tuple[Any, ...], Unknown]]` cannot be called with key of type `Hashable` on object of type `ndarray[Any, Any]`
- static_frame/core/index_hierarchy.py:2165:44 error[unresolved-attribute] Object of type `Hashable & ~int & ~Top[integer[Any]] & ~IndexHierarchy & ~Top[list[Unknown]]` has no attribute `ndim`
- static_frame/core/index_hierarchy.py:2166:16 error[unresolved-attribute] Object of type `Hashable & ~int & ~Top[integer[Any]] & ~IndexHierarchy & ~Top[list[Unknown]]` has no attribute `dtype`
- static_frame/core/index_hierarchy.py:2169:35 error[invalid-argument-type] Argument to function `view_2d_as_1d` is incorrect: Expected `ndarray[Any, Any]`, found `Hashable & ~int & ~Top[integer[Any]] & ~IndexHierarchy & ~Top[list[Unknown]]`
- static_frame/core/index_hierarchy.py:2177:72 error[not-iterable] Object of type `Hashable & ~int & ~Top[integer[Any]] & ~IndexHierarchy & ~Top[list[Unknown]]` is not iterable
- static_frame/core/index_hierarchy.py:2266:70 error[invalid-argument-type] Argument to bound method `SortStatus.derive_status_from_slice` is incorrect: Expected `slice[Any, Any, Any]`, found `(ndarray[Any, Any] & ~int & ~Top[integer[Any]]) | (list[int] & ~Top[integer[Any]]) | slice[Any, Any, Any]`
+ static_frame/core/index_hierarchy.py:566:41 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ static_frame/core/index_hierarchy.py:567:37 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ static_frame/core/index_hierarchy.py:568:30 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- static_frame/core/index_hierarchy.py:2173:51 error[not-iterable] Object of type `Hashable & ~int & ~Top[integer[Any]] & ~IndexHierarchy & ~Top[list[Unknown]]` is not iterable
+ static_frame/core/index_hierarchy.py:2173:51 error[not-iterable] Object of type `ndarray[tuple[object, ...], dtype[object]] & ~int & ~Top[integer[Any]] & ~IndexHierarchy & ~Top[list[Unknown]]` is not iterable
- static_frame/core/loc_map.py:187:32 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- static_frame/core/loc_map.py:190:69 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- static_frame/core/loc_map.py:191:61 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- static_frame/core/loc_map.py:199:41 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- static_frame/core/loc_map.py:202:68 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- static_frame/core/loc_map.py:204:60 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- static_frame/core/loc_map.py:207:88 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- static_frame/core/loc_map.py:209:55 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- static_frame/core/loc_map.py:210:40 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- static_frame/core/pivot.py:248:44 error[unresolved-attribute] Attribute `dtype` is not defined on `list[Any]` in union `list[Any] | ndarray[Any, Any]`
- static_frame/core/pivot.py:249:16 error[unresolved-attribute] Attribute `dtype` is not defined on `list[Any]` in union `list[Any] | ndarray[Any, Any]`
- static_frame/core/pivot.py:252:13 error[invalid-assignment] Invalid subscript assignment with key of type `list[int]` and value of type `Any` on object of type `list[Any]`
- static_frame/core/pivot.py:253:13 error[unresolved-attribute] Attribute `flags` is not defined on `list[Any]` in union `list[Any] | ndarray[Any, Any]`
- static_frame/core/pivot.py:260:13 error[unresolved-attribute] Attribute `flags` is not defined on `list[Any]` in union `list[Any] | ndarray[Any, Any]`
- static_frame/core/pivot.py:250:25 error[unresolved-attribute] Attribute `astype` is not defined on `list[Any]` in union `list[Any] | ndarray[Any, Any]`
+ static_frame/core/pivot.py:250:25 error[no-matching-overload] No overload of bound method `ndarray.astype` matches arguments
- static_frame/core/reduce.py:462:21 error[unresolved-attribute] Attribute `flags` is not defined on `list[Any]` in union `ndarray[tuple[int], dtype[Any]] | list[Any] | ndarray[Any, Any]`
- static_frame/core/reduce.py:463:35 error[invalid-argument-type] Argument to bound method `list.append` is incorrect: Expected `ndarray[Any, Any]`, found `ndarray[tuple[int], dtype[Any]] | list[Any] | ndarray[Any, Any]`
- static_frame/core/reduce.py:486:21 error[unresolved-attribute] Attribute `flags` is not defined on `list[Any]` in union `ndarray[tuple[int], dtype[Any]] | list[Any] | ndarray[Any, Any]`
- static_frame/core/reduce.py:487:35 error[invalid-argument-type] Argument to bound method `list.append` is incorrect: Expected `ndarray[Any, Any]`, found `ndarray[tuple[int], dtype[Any]] | list[Any] | ndarray[Any, Any]`
- static_frame/core/series.py:2130:44 error[unresolved-attribute] Attribute `dtype` is not defined on `int`, `list[int]`, `slice[Any, Any, Any]`, `None` in union `int | integer[Any] | ndarray[Any, Any] | ... omitted 3 union elements`
- static_frame/core/type_blocks.py:1639:36 error[unresolved-attribute] Attribute `start` is not defined on `int` in union `slice[Any, Any, Any] | int`
- static_frame/core/type_blocks.py:1640:35 error[unresolved-attribute] Attribute `stop` is not defined on `int` in union `slice[Any, Any, Any] | int`
- static_frame/core/type_blocks.py:1760:25 error[unresolved-attribute] Attribute `start` is not defined on `int` in union `slice[Any, Any, Any] | int`
- static_frame/core/type_blocks.py:1761:28 error[unresolved-attribute] Attribute `start` is not defined on `int` in union `slice[Any, Any, Any] | int`
- static_frame/core/type_blocks.py:1765:25 error[unresolved-attribute] Attribute `stop` is not defined on `int` in union `slice[Any, Any, Any] | int`
- static_frame/core/type_blocks.py:1765:46 error[unresolved-attribute] Attribute `stop` is not defined on `int` in union `slice[Any, Any, Any] | int`
- static_frame/core/type_blocks.py:1842:40 error[unresolved-attribute] Attribute `start` is not defined on `None`, `int` in union `None | slice[Any, Any, Any] | int`
- static_frame/core/type_blocks.py:1843:39 error[unresolved-attribute] Attribute `stop` is not defined on `None`, `int` in union `None | slice[Any, Any, Any] | int`
- static_frame/core/type_blocks.py:1890:48 error[unresolved-attribute] Attribute `dtype` is not defined on `int`, `list[int]`, `slice[Any, Any, Any]`, `None` in union `int | integer[Any] | ndarray[Any, Any] | ... omitted 3 union elements`
- static_frame/core/type_blocks.py:1892:50 error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `int | integer[Any] | ndarray[Any, Any] | ... omitted 3 union elements`
- static_frame/core/type_blocks.py:1931:40 error[unresolved-attribute] Attribute `start` is not defined on `None`, `int` in union `None | slice[Any, Any, Any] | int`
- static_frame/core/type_blocks.py:1932:39 error[unresolved-attribute] Attribute `stop` is not defined on `None`, `int` in union `None | slice[Any, Any, Any] | int`
- static_frame/core/type_blocks.py:2377:27 error[unresolved-attribute] Object of type `object` has no attribute `dtype`
- static_frame/core/type_blocks.py:2726:16 error[unresolved-attribute] Attribute `dtype` is not defined on `list[int] & ~Top[integer[Any]]`, `slice[Any, Any, Any]`, `None` in union `(ndarray[Any, Any] & ~int & ~Top[integer[Any]]) | (list[int] & ~Top[integer[Any]]) | slice[Any, Any, Any] | None`
- static_frame/core/type_blocks.py:2727:20 error[unresolved-attribute] Attribute `sum` is not defined on `list[int] & ~Top[integer[Any]]`, `slice[Any, Any, Any]`, `None` in union `(ndarray[Any, Any] & ~int & ~Top[integer[Any]]) | (list[int] & ~Top[integer[Any]]) | slice[Any, Any, Any] | None`
- static_frame/core/type_blocks.py:2729:22 error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `(ndarray[Any, Any] & ~int & ~Top[integer[Any]]) | (list[int] & ~Top[integer[Any]]) | slice[Any, Any, Any] | None`
- static_frame/core/type_blocks.py:3352:16 error[unresolved-attribute] Attribute `ndim` is not defined on `Iterable[Any] & ~TypeBlocks` in union `(Iterable[Any] & ~TypeBlocks) | ndarray[Any, Any]`
- static_frame/core/type_blocks.py:3352:36 error[unresolved-attribute] Attribute `ndim` is not defined on `Iterable[Any] & ~TypeBlocks` in union `(Iterable[Any] & ~TypeBlocks) | ndarray[Any, Any]`
- static_frame/core/type_blocks.py:3352:60 error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `(Iterable[Any] & ~TypeBlocks) | ndarray[Any, Any]`
- static_frame/core/type_blocks.py:3355:34 error[invalid-assignment] Object of type `GeneratorType[(Iterable[Any] & ~TypeBlocks) | ndarray[Any, Any], None, None]` is not assignable to `Iterable[ndarray[Any, Any]]`
- static_frame/core/type_blocks.py:3356:18 error[unresolved-attribute] Attribute `ndim` is not defined on `Iterable[Any] & ~TypeBlocks` in union `(Iterable[Any] & ~TypeBlocks) | ndarray[Any, Any]`
- static_frame/core/type_blocks.py:3357:38 error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `(Iterable[Any] & ~TypeBlocks) | ndarray[Any, Any]`
- static_frame/core/type_blocks.py:3361:40 error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `(Iterable[Any] & ~TypeBlocks) | ndarray[Any, Any]`
- static_frame/core/type_blocks.py:3365:107 error[unresolved-attribute] Attribute `shape` is not defined on `Iterable[Any] & ~TypeBlocks` in union `(Iterable[Any] & ~TypeBlocks) | ndarray[Any, Any]`
- static_frame/core/type_blocks.py:3367:18 error[unresolved-attribute] Attribute `ndim` is not defined on `Iterable[Any] & ~TypeBlocks` in union `(Iterable[Any] & ~TypeBlocks) | ndarray[Any, Any]`
- static_frame/core/type_blocks.py:3367:38 error[unresolved-attribute] Attribute `shape` is not defined on `Iterable[Any] & ~TypeBlocks` in union `(Iterable[Any] & ~TypeBlocks) | ndarray[Any, Any]`
- static_frame/core/type_blocks.py:3374:111 error[unresolved-attribute] Attribute `shape` is not defined on `Iterable[Any] & ~TypeBlocks` in union `(Iterable[Any] & ~TypeBlocks) | ndarray[Any, Any]`
+ static_frame/core/type_blocks.py:1642:50 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ static_frame/core/type_blocks.py:1643:53 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ static_frame/core/type_blocks.py:1768:50 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ static_frame/core/type_blocks.py:1769:53 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ static_frame/core/type_blocks.py:2177:53 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ static_frame/core/type_blocks.py:2178:51 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ static_frame/core/type_blocks.py:2272:49 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ static_frame/core/type_blocks.py:2292:71 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- static_frame/core/type_blocks.py:2182:30 error[unsupported-operator] Operator `+` is not supported between objects of type `None | slice[Any, Any, Any] | int` and `Literal[1]`
+ static_frame/core/type_blocks.py:2182:30 error[unsupported-operator] Operator `+` is not supported between objects of type `None | int` and `Literal[1]`
- static_frame/core/type_blocks.py:2266:17 error[invalid-declaration] Cannot declare type `int` for inferred type `None | slice[Any, Any, Any] | int | Any`
+ static_frame/core/type_blocks.py:2266:17 error[invalid-declaration] Cannot declare type `int` for inferred type `None | int | Any`
- static_frame/core/type_blocks.py:2275:21 error[unsupported-operator] Operator `>` is not supported between objects of type `None | slice[Any, Any, Any] | int | Any` and `Literal[0] | Unknown`
+ static_frame/core/type_blocks.py:2275:21 error[unsupported-operator] Operator `>` is not supported between objects of type `None | int | Any` and `Literal[0] | Unknown`
- static_frame/core/type_blocks.py:2310:33 error[unsupported-operator] Operator `+` is not supported between objects of type `None | slice[Any, Any, Any] | int | Any` and `Any | Literal[1]`
+ static_frame/core/type_blocks.py:2310:33 error[unsupported-operator] Operator `+` is not supported between objects of type `None | int | Any` and `Any | Literal[1]`
- static_frame/core/type_blocks.py:3360:39 error[not-subscriptable] Cannot subscript object of type `Iterable[Any]` with no `__getitem__` method
+ static_frame/core/type_blocks.py:3360:39 error[invalid-argument-type] Method `__getitem__` of type `Overload[(key: ndarray[tuple[Any, ...], dtype[integer[Any] | numpy.bool[builtins.bool]]] | tuple[ndarray[tuple[Any, ...], dtype[integer[Any] | numpy.bool[builtins.bool]]], ...], /) -> ndarray[tuple[Any, ...], dtype[object]], (key: SupportsIndex | tuple[SupportsIndex, ...], /) -> Any, (key: SupportsIndex | slice[Any, Any, Any] | EllipsisType | ... omitted 5 union elements, /) -> ndarray[tuple[Any, ...], dtype[object]], (key: str, /) -> ndarray[tuple[object, ...], dtype[Any]], (key: list[str], /) -> ndarray[tuple[object, ...], Unknown]]` cannot be called with key of type `slice[Any, Any, Any]` on object of type `ndarray[tuple[object, ...], dtype[object]]`
- static_frame/core/type_blocks.py:3370:21 error[not-subscriptable] Cannot subscript object of type `Iterable[Any]` with no `__getitem__` method
+ static_frame/core/type_blocks.py:3370:21 error[invalid-argument-type] Method `__getitem__` of type `Overload[(key: ndarray[tuple[Any, ...], dtype[integer[Any] | numpy.bool[builtins.bool]]] | tuple[ndarray[tuple[Any, ...], dtype[integer[Any] | numpy.bool[builtins.bool]]], ...], /) -> ndarray[tuple[Any, ...], dtype[object]], (key: SupportsIndex | tuple[SupportsIndex, ...], /) -> Any, (key: SupportsIndex | slice[Any, Any, Any] | EllipsisType | ... omitted 5 union elements, /) -> ndarray[tuple[Any, ...], dtype[object]], (key: str, /) -> ndarray[tuple[object, ...], dtype[Any]], (key: list[str], /) -> ndarray[tuple[object, ...], Unknown]]` cannot be called with key of type `tuple[slice[Any, Any, Any], slice[Any, Any, Any]]` on object of type `ndarray[tuple[object, ...], dtype[object]]`
- static_frame/core/util.py:446:12 error[unresolved-attribute] Attribute `dtype` is not defined on `int`, `list[int]`, `slice[Any, Any, Any]` in union `int | list[int] | slice[Any, Any, Any] | ndarray[Any, dtype[signedinteger[_64Bit]]]`
- static_frame/core/util.py:448:14 error[unresolved-attribute] Attribute `dtype` is not defined on `int`, `list[int]`, `slice[Any, Any, Any]` in union `int | list[int] | slice[Any, Any, Any] | ndarray[Any, dtype[signedinteger[_64Bit]]]`
- static_frame/core/util.py:450:14 error[unresolved-attribute] Attribute `dtype` is not defined on `int`, `list[int]`, `slice[Any, Any, Any]` in union `int | list[int] | slice[Any, Any, Any] | ndarray[Any, dtype[signedinteger[_64Bit]]]`
- static_frame/core/util.py:455:73 error[unresolved-attribute] Attribute `dtype` is not defined on `int`, `list[int]`, `slice[Any, Any, Any]` in union `int | list[int] | slice[Any, Any, Any] | ndarray[Any, dtype[signedinteger[_64Bit]]]`
- static_frame/core/util.py:457:12 error[unresolved-attribute] Attribute `start` is not defined on `int`, `list[int]`, `ndarray[Any, dtype[signedinteger[_64Bit]]]` in union `int | list[int] | slice[Any, Any, Any] | ndarray[Any, dtype[signedinteger[_64Bit]]]`
- static_frame/core/util.py:457:56 error[unresolved-attribute] Attribute `start` is not defined on `int`, `list[int]`, `ndarray[Any, dtype[signedinteger[_64Bit]]]` in union `int | list[int] | slice[Any, Any, Any] | ndarray[Any, dtype[signedinteger[_64Bit]]]`
- static_frame/core/util.py:459:12 error[unresolved-attribute] Attribute `stop` is not defined on `int`, `list[int]`, `ndarray[Any, dtype[signedinteger[_64Bit]]]` in union `int | list[int] | slice[Any, Any, Any] | ndarray[Any, dtype[signedinteger[_64Bit]]]`
- static_frame/core/util.py:459:55 error[unresolved-attribute] Attribute `stop` is not defined on `int`, `list[int]`, `ndarray[Any, dtype[signedinteger[_64Bit]]]` in union `int | list[int] | slice[Any, Any, Any] | ndarray[Any, dtype[signedinteger[_64Bit]]]`
- static_frame/core/util.py:461:28 error[unresolved-attribute] Attribute `indices` is not defined on `int`, `list[int]`, `ndarray[Any, dtype[signedinteger[_64Bit]]]` in union `int | list[int] | slice[Any, Any, Any] | ndarray[Any, dtype[signedinteger[_64Bit]]]`
- static_frame/core/util.py:2098:19 error[unresolved-attribute] Object of type `Iterable[Any]` has no attribute `start`
- static_frame/core/util.py:2099:18 error[unresolved-attribute] Object of type `Iterable[Any]` has no attribute `stop`
- static_frame/core/util.py:2100:18 error[unresolved-attribute] Object of type `Iterable[Any]` has no attribute `step`
- static_frame/core/util.py:451:22 error[not-iterable] Object of type `int | list[int] | slice[Any, Any, Any] | ndarray[Any, dtype[signedinteger[_64Bit]]]` may not be iterable
+ static_frame/core/util.py:451:22 error[not-iterable] Object of type `(int & ndarray[tuple[object, ...], dtype[object]]) | (list[int] & ndarray[tuple[object, ...], dtype[object]]) | (ndarray[Any, dtype[signedinteger[_64Bit]]] & ndarray[tuple[object, ...], dtype[object]])` is not iterable

sympy (https://github.com/sympy/sympy)
- sympy/matrices/matrixbase.py:5706:12 error[invalid-return-type] Return type does not match returned value: expected `tuple[MatrixBase, MatrixBase | Expr, str]`, found `tuple[MatrixBase | Unknown, None | MatrixBase | Expr | Unknown, Literal["invalid_type", "is_matrix", "possible_scalar"]]`
+ sympy/matrices/matrixbase.py:5706:12 error[invalid-return-type] Return type does not match returned value: expected `tuple[MatrixBase, MatrixBase | Expr, str]`, found `tuple[MatrixBase | Unknown, None | Expr | MatrixBase | Unknown, Literal["invalid_type", "is_matrix", "possible_scalar"]]`

werkzeug (https://github.com/pallets/werkzeug)
- src/werkzeug/datastructures/headers.py:107:33 error[unresolved-attribute] Object of type `object` has no attribute `_list`
- tests/test_wrappers.py:368:16 error[unresolved-attribute] Object of type `Response` has no attribute `foo`

xarray (https://github.com/pydata/xarray)
- xarray/indexes/nd_point_index.py:302:38 error[invalid-argument-type] Argument to bound method `TreeAdapter.equals` is incorrect: Argument type `object` does not satisfy upper bound `TreeAdapter` of type variable `Self`
- xarray/indexes/nd_point_index.py:302:38 error[invalid-argument-type] Argument to bound method `TreeAdapter.equals` is incorrect: Expected `T_TreeAdapter@NDPointIndex`, found `object`

Full report with detailed diff (timing results)

@charliermarsh
charliermarsh force-pushed the charlie/dunder-class-narrowing branch 3 times, most recently from 39c0f0c to 85b1ba5 Compare May 5, 2026 02:42
@charliermarsh
charliermarsh changed the base branch from charlie/protocol-recursion to main May 5, 2026 02:42
@codspeed

codspeed Bot commented May 5, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 117 untouched benchmarks


Comparing charlie/dunder-class-narrowing (8d51a70) with main (bd66629)

Open in CodSpeed

@charliermarsh
charliermarsh force-pushed the charlie/dunder-class-narrowing branch from 85b1ba5 to bb72d84 Compare May 5, 2026 02:56
@charliermarsh
charliermarsh marked this pull request as ready for review May 5, 2026 03:01
@charliermarsh
charliermarsh force-pushed the charlie/dunder-class-narrowing branch from bb72d84 to cc95bfa Compare May 6, 2026 20:56

@ibraheemdev ibraheemdev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Code looks good to me, but I'll leave the decision on whether we should be narrowing on __class__ for someone else to approve. It looks like pyright does not do this, but mypy does.

Comment thread crates/ty_python_core/src/place.rs
Comment thread crates/ty_python_semantic/src/types/narrow.rs
Comment thread crates/ty_python_semantic/src/types/narrow.rs
@AlexWaygood

Copy link
Copy Markdown
Member

I'll leave the decision on whether we should be narrowing on __class__ for someone else to approve. It looks like pyright does not do this, but mypy does.

For me this one makes sense. I think it's reasonably common and just as idiomatic as if type(x) is Foo narrowing, which we already support. The ecosystem results show it has a decent amount of impact for our users.

@AlexWaygood

Copy link
Copy Markdown
Member

Although these can differ at runtime, AFAICT we already treat these as the same internally (i.e., infer_builtins_type_call calls arg_type.dunder_class(db)).

(And they can only differ in extremely dynamic cases, which a type checker cannot reasonably be expected to support or understand, so I don't think we need to really worry about that.)

@charliermarsh
charliermarsh force-pushed the charlie/dunder-class-narrowing branch from cc95bfa to 8d51a70 Compare May 8, 2026 00:14
@charliermarsh
charliermarsh enabled auto-merge (squash) May 8, 2026 00:17
@charliermarsh
charliermarsh merged commit 3a49028 into main May 8, 2026
54 of 56 checks passed
@charliermarsh
charliermarsh deleted the charlie/dunder-class-narrowing branch May 8, 2026 00:18
carljm added a commit that referenced this pull request May 8, 2026
* main: (23 commits)
  Lazily initialize `UnionBuilder` when transforming a `Union` (#25050)
  [ty] Speed-up find-references by using multithreading for cross-file searches (#25042)
  Revert "[ty] Lazily initialize `UnionBuilder` when transforming a union type" (#25049)
  [ty] Make text-based find-references pre-filter identifier boundary aware (#25036)
  [ty] Fix cross-file find-references for keyword arguments (#25043)
  [ty] Include TypedDict type context when inferring mixed constructors (#25039)
  [ty] Include TypedDict type context when inferring string keys (#25037)
  [ty] Lazily initialize `UnionBuilder` when transforming a union type (#24929)
  [ty] Skip global search for references if identifier is not externally visible (#25033)
  [ty] Support narrowing on `__class__` checks (#24997)
  [ty] Bump ecosystem-analyzer (#25032)
  [ty] Include error context for overload consistency diagnostics (#24950)
  [ty] Check non-generic overload implementations (#24936)
  Filter overloads based on return type for ParamSpec mapping (#24769)
  [ty] Add versioned parameters for dataclass decorator (#25029)
  [ty] Project reachability constraints before narrowing (#24982)
  [ty] Emit folding ranges from the language server for multi-line block headers. (#24978)
  [ty] Allow ParamSpec specialization through unioned generic classes (#24826)
  [ty] Adjust start of block folding range to preserve visible header for character-precise LSP clients. (#24917)
  [ty] Use `as_i64` when comparing `IntLiteralType` instances (#25023)
  ...
thejchap pushed a commit to thejchap/ruff that referenced this pull request May 23, 2026
## Summary

This adds support for narrowing based on `__class__` identity checks,
treating them the same way we already treat `type(...)` checks. Although
these _can_ differ at runtime, AFAICT we already treat these as the same
internally (i.e., `infer_builtins_type_call` calls
`arg_type.dunder_class(db)`).

For example:

```python
from typing import final, reveal_type

class A: ...
class B: ...

@Final
class C: ...

def f(x: A | B, y: A | C):
    if x.__class__ is A:
        reveal_type(x)  # revealed: A

    if C is y.__class__:
        reveal_type(y)  # revealed: C
    else:
        reveal_type(y)  # revealed: A
```

This supports both operand orders, and both `is` and `is not`. As with
`type(x) is C`, negative-branch narrowing is only applied when the
tested class is final.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty The ty type checker

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants