Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
cdffb1d
[ty] Relaxed `isinstance` narrowing
sharkdp Jun 25, 2026
a60d1af
[ty] Relaxed `isinstance` narrowing through special top/bottom materi…
sharkdp Jul 14, 2026
fcd8fee
Performance work
sharkdp Jul 14, 2026
e885df8
Restructuring
sharkdp Jul 14, 2026
16a8f4e
Minor changes
sharkdp Jul 14, 2026
a894808
Ordering
sharkdp Jul 14, 2026
5af7d74
Reformulation
sharkdp Jul 14, 2026
87178c7
Remove codex generated tests
sharkdp Jul 14, 2026
b16cab1
Callable update
sharkdp Jul 14, 2026
be18ab1
Terminology
sharkdp Jul 14, 2026
a1c03cc
More renaming
sharkdp Jul 14, 2026
3a1e502
Comments
sharkdp Jul 14, 2026
8ee95ec
Fixes
sharkdp Jul 14, 2026
877f14e
[ty] Experiment with tagged narrowing bounds
sharkdp Jul 15, 2026
6443890
[ty] Preserve tagged generic narrowing bounds
sharkdp Jul 28, 2026
4a44769
[ty] Make tagged narrowing bounds gradual for assignability
sharkdp Jul 28, 2026
f11919b
[ty] Experiment with specialization-aware isinstance filtering
sharkdp Jul 29, 2026
1d0df1d
[ty] Preserve TypedDict interfaces during generic narrowing
sharkdp Jul 30, 2026
ddc396e
[ty] Fix filtering branch CI failures
sharkdp Aug 3, 2026
cce6a54
[ty] Simplify generic narrowing implementation
sharkdp Aug 5, 2026
4419b66
[ty] Apply generic narrowing settings to class patterns
sharkdp Aug 5, 2026
ba249b3
Manual review
sharkdp Aug 5, 2026
7db043c
Manual review pt2
sharkdp Aug 5, 2026
6a471b9
Rename
sharkdp Aug 5, 2026
98e6033
Manual review pt3
sharkdp Aug 5, 2026
ee1801c
Manual review pt4
sharkdp Aug 5, 2026
4575f6e
Manual review pt5
sharkdp Aug 5, 2026
3de2e88
Address narrowing to Base classes
sharkdp Aug 5, 2026
68d719c
[ty] Restore strict generic class pattern specialization
sharkdp Aug 6, 2026
0a16895
[ty] Combine specialized bases when narrowing intersections
sharkdp Aug 6, 2026
e7d1683
[ty] Preserve specialization when narrowing structural protocols
sharkdp Aug 6, 2026
bdd2830
[ty] Preserve known callable signatures during narrowing
sharkdp Aug 6, 2026
2578b89
[ty] Restore strict generic default narrowing coverage
sharkdp Aug 6, 2026
6c4628c
Wording
sharkdp Aug 6, 2026
885be27
[ty] Cover bounded generic defaults in gradual mode
sharkdp Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/ruff_benchmark/benches/ty_walltime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ static ALTAIR: Benchmark = Benchmark::new(
max_dep_date: TY_ECOSYSTEM_PIN,
python_version: SupportedPythonVersion::Py311,
},
5,
9,
Comment thread
sharkdp marked this conversation as resolved.
);

static COLOUR_SCIENCE: Benchmark = Benchmark::new(
Expand Down
86 changes: 86 additions & 0 deletions crates/ty/docs/configuration.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions crates/ty_project/src/metadata/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1438,6 +1438,32 @@ pub struct TerminalOptions {
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct AnalysisOptions {
/// Whether ty should use strict narrowing for unspecialized generic classes in
/// `isinstance()` and `issubclass()` checks, as well as `match` class patterns.
///
/// When enabled, ty narrows to the top materialization of the class. For example,
/// `isinstance(value, list)` narrows a value of type `object` to `Top[list[Unknown]]`,
/// representing the (infinite) union of all possible `list` specializations. Iterating
/// over the list would yield values of type `object`.
///
/// When disabled, ty uses gradual generic narrowing, preserving compatible type
/// arguments from the original type where possible. For example,
/// `isinstance(value, list)` narrows a value of type `Sequence[int]` to `list[int]`.
/// If no specialization is available, the same check narrows a value of type `object`
/// to `list[Unknown]`; items of any type can then be appended to the list. Class
/// patterns such as `case list():` follow the same behavior.
///
/// Defaults to `false`.
#[option(
default = r#"false"#,
value_type = "bool",
example = r#"
# Use the top materialization when narrowing to an unspecialized generic class
strict-generic-narrowing = true
"#
)]
pub strict_generic_narrowing: Option<bool>,

/// Configure ty's behavior regarding type inference and narrowing of equality
/// checks. Defaults to `false`.
///
Expand Down Expand Up @@ -1604,13 +1630,15 @@ impl AnalysisOptions {
diagnostics: &mut Vec<OptionDiagnostic>,
) -> AnalysisSettings {
let Self {
strict_generic_narrowing,
strict_equality_semantics,
respect_type_ignore_comments,
allowed_unresolved_imports,
replace_imports_with_any,
} = self;

let AnalysisSettings {
strict_generic_narrowing: strict_generic_narrowing_default,
strict_equality_semantics: strict_equality_semantics_default,
respect_type_ignore_comments: respect_type_ignore_default,
allowed_unresolved_imports: allowed_unresolved_imports_default,
Expand Down Expand Up @@ -1640,6 +1668,8 @@ impl AnalysisOptions {
};

AnalysisSettings {
strict_generic_narrowing: strict_generic_narrowing
.unwrap_or(strict_generic_narrowing_default),
strict_equality_semantics: strict_equality_semantics
.unwrap_or(strict_equality_semantics_default),
respect_type_ignore_comments: respect_type_ignore_comments
Expand Down
10 changes: 10 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/call/builtins.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,11 @@ for function in map(Function, [object()]):
Several `dict` overloads accept one positional argument. When none matches, an arbitrarily selected
overload must not make an otherwise compatible return type fail.

```toml
[analysis]
strict-generic-narrowing = true
```
Comment thread
sharkdp marked this conversation as resolved.

```py
from collections.abc import Mapping

Expand All @@ -531,6 +536,11 @@ def copy(value: object) -> dict[str, str]:
An invalid `dict` call must not invalidate an assignment inside a branch where the original value
has already been narrowed to a mapping.

```toml
[analysis]
strict-generic-narrowing = true
```

```py
from collections.abc import Mapping

Expand Down
24 changes: 9 additions & 15 deletions crates/ty_python_semantic/resources/mdtest/loops/for.md
Original file line number Diff line number Diff line change
Expand Up @@ -679,14 +679,13 @@ def _(x: Sequence[int], y: object):
reveal_type(item) # revealed: int

if isinstance(y, list):
reveal_type(y) # revealed: Top[list[Unknown]]
reveal_type(y) # revealed: list[Unknown]
for item in y:
reveal_type(item) # revealed: object
reveal_type(item) # revealed: Unknown

if isinstance(x, list):
reveal_type(x) # revealed: Sequence[int] & Top[list[Unknown]]
reveal_type(x) # revealed: list[int]
for item in x:
# int & object simplifies to int
reveal_type(item) # revealed: int
```

Expand Down Expand Up @@ -1541,12 +1540,10 @@ simplify to `Never`, leaving only the iterable parts.
```py
def f[T: tuple[int, ...] | int](x: T):
if isinstance(x, tuple):
reveal_type(x) # revealed: T@f & tuple[object, ...]
reveal_type(x) # revealed: T@f & tuple[int, ...]
for item in x:
# The intersection `(tuple[int, ...] | int) & tuple[object, ...]` distributes to:
# `(tuple[int, ...] & tuple[object, ...]) | (int & tuple[object, ...])`
# which simplifies to `tuple[int, ...] | Never` = `tuple[int, ...]`
# so iterating gives `int`.
# The `int` alternative in the TypeVar bound is disjoint from `tuple`. The
# remaining `tuple[int, ...]` alternative supplies the narrowed specialization.
reveal_type(item) # revealed: int
```

Expand All @@ -1558,13 +1555,10 @@ constraint, those parts should also simplify to `Never`.
```py
def g[T: tuple[int, ...] | list[str]](x: T):
if isinstance(x, tuple):
reveal_type(x) # revealed: T@g & tuple[object, ...]
reveal_type(x) # revealed: T@g & tuple[int, ...]
for item in x:
# The intersection `(tuple[int, ...] | list[str]) & tuple[object, ...]` distributes to:
# `(tuple[int, ...] & tuple[object, ...]) | (list[str] & tuple[object, ...])`
# Since `list[str]` is disjoint from `tuple[object, ...]`, this simplifies to:
# `tuple[int, ...] | Never` = `tuple[int, ...]`
# so iterating gives `int`, NOT `int | str`.
# The `list[str]` alternative in the TypeVar bound is disjoint from `tuple`. The
# remaining `tuple[int, ...]` alternative supplies the narrowed specialization.
reveal_type(item) # revealed: int
```

Expand Down
41 changes: 38 additions & 3 deletions crates/ty_python_semantic/resources/mdtest/narrow/callable.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,15 @@ def f(x: object):

## Calling narrowed callables

The narrowed type `Top[Callable[..., object]]` represents the set of all possible callable types
### Strict generic narrowing mode

```toml
[analysis]
strict-generic-narrowing = true
```

In strict generic narrowing mode, an `isinstance(.., Callable)` check intersects the type with
`Top[Callable[..., object]]`. This type represents the set of all possible callable types
(including, e.g., functions that take no arguments and functions that require arguments). While such
objects *are* callable (they pass `callable()`), no specific set of arguments can be guaranteed to
be valid.
Expand All @@ -80,6 +88,28 @@ def resolve(value: str):
reveal_type(value()) # revealed: object
```

### Gradual generic narrowing mode

```toml
[analysis]
strict-generic-narrowing = false
```

In gradual generic narrowing mode, an `isinstance(.., Callable)` check narrows to a gradual
callable. Its parameters accept arbitrary arguments, and its return type is `Unknown`:

```py
from typing import Callable

def call_with_args(y: object):
if isinstance(y, Callable):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Note that this means that our default behavior here for isinstance(.., Callable) will be different from a callable(..) check, which always uses the top materialization. This was previously discussed as an acceptable compromise, if I understood our discussion correctly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't remember details of that discussion. I think this is probably acceptable for now, though I think ideally these would have the same semantics. I guess in order to do that we would need to special-case callable (so the top materialization of its return type can depend on the strict-narrowing setting) rather than just patching typeshed...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Previous discussion here: #26797 (comment). I will open a ticket to address this as a follow-up.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

reveal_type(y) # revealed: (...) -> Unknown

reveal_type(y()) # revealed: Unknown
reveal_type(y(1, "foo")) # revealed: Unknown
reveal_type(y(1, "foo", keyword_arg="bar")) # revealed: Unknown
```

## Narrowing with named expressions (walrus operator)

When `callable()` is used with a named expression, the target of the named expression should be
Expand Down Expand Up @@ -139,9 +169,14 @@ import collections.abc

def f(x: object):
if isinstance(x, typing.Callable):
reveal_type(x) # revealed: Top[(...) -> object]
reveal_type(x) # revealed: (...) -> Unknown
else:
reveal_type(x) # revealed: ~Top[(...) -> object]

if isinstance(x, collections.abc.Callable):
reveal_type(x) # revealed: Top[(...) -> object]
reveal_type(x) # revealed: (...) -> Unknown
else:
reveal_type(x) # revealed: ~Top[(...) -> object]
```

## `Callable` special-form identity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,11 @@ After the `isinstance` check, `values` has type `Iterable[Literal[1]] & tuple[ob
semantics were checked: the `tuple` component establishes that membership compares against its
elements, while the `Iterable` component constrains those elements to `Literal[1]`.

```toml
[analysis]
strict-generic-narrowing = true
```

Comment thread
sharkdp marked this conversation as resolved.
```py
from collections.abc import Iterable
from typing import Literal, final
Expand Down
Loading
Loading