Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
f39370a
[ty] Support calls to intersection types
claude Jan 9, 2026
b3b1b7b
Fix markdown formatting in intersection_types.md
claude Jan 9, 2026
360f1e9
[ty] Show individual errors when all intersection elements fail a call
claude Jan 9, 2026
6b4a0b8
Bump sympy expected diagnostics to 13200
claude Jan 9, 2026
4866f6e
[ty] Add priority hierarchy for intersection call errors
claude Jan 9, 2026
bc4798f
[ty] Support unions containing intersection elements in call bindings
claude Jan 9, 2026
cc1ddc5
Merge main into claude/fix-issue-1858-UjARA
claude Jan 9, 2026
930f13d
Bump freqtrade expected diagnostics from 600 to 650
claude Jan 9, 2026
01b1937
Fix invalid-await for intersections containing dynamic types
claude Jan 9, 2026
fafd6e2
cargo fmt
carljm Jan 9, 2026
d9b22cf
fix up 3.12/3.13 testing
carljm Jan 9, 2026
feb4df1
review tweaks
carljm Jan 9, 2026
c04fc6b
Fix: merge argument forms instead of overwriting
claude Jan 12, 2026
849e267
Fix: unify as_result() logic for intersection and single-binding cases
claude Jan 12, 2026
f0c0c77
Fix misleading comment about Bindings structure
claude Jan 12, 2026
ac66d4c
Improve iter()/iter_mut() documentation and add BindingsElement::is_c…
claude Jan 12, 2026
fee0888
Move retain logic into retain_successful() method
claude Jan 12, 2026
fa9474f
Layer UnionDiagnostic and IntersectionDiagnostic for nested types
claude Jan 12, 2026
f4da250
Merge main into claude/fix-issue-1858-UjARA
claude Jan 12, 2026
c440f4b
Refactor dataclass params to use with_dataclass_params helper
claude Jan 12, 2026
48c3b5e
Remove unused ClassLiteral import
claude Jan 12, 2026
7a10c64
cargo fmt
carljm Jan 13, 2026
7926fb1
Fix dataclass transform function handling after merge
carljm Jan 13, 2026
110ae7c
prek
carljm Jan 13, 2026
7247fb8
Merge branch 'main' into claude/fix-issue-1858-UjARA
carljm Jan 20, 2026
6450fdc
Merge branch 'main' into claude/fix-issue-1858-UjARA
carljm Jan 23, 2026
a43f1ce
Merge branch 'main' into claude/fix-issue-1858-UjARA
carljm Jan 30, 2026
d330ae7
Merge branch 'main' into claude/fix-issue-1858-UjARA
carljm Feb 2, 2026
294ba70
Merge branch 'main' into claude/fix-issue-1858-UjARA
carljm Feb 14, 2026
6cc0859
clippy and fmt
carljm Feb 15, 2026
abec213
fix dataclass changes lost in merge
carljm Feb 15, 2026
c91bc66
add comments about safe use of intersection.positive()
carljm Feb 15, 2026
c8d4a76
fix bug with unions-within-unions
carljm Feb 15, 2026
8d7d3ee
add more test cases
carljm Feb 15, 2026
8a21abc
preserve return type info from top callables
carljm Feb 15, 2026
1d7368d
Use Bindings::iter and Bindings::iter_mut more
carljm Feb 15, 2026
6e90a66
Add Bindings::map_types and fix dataclass descriptor handling
carljm Feb 15, 2026
a7ce0fe
remove merge artifact
carljm Feb 15, 2026
82d3ccf
Merge branch 'main' into claude/fix-issue-1858-UjARA
carljm Feb 15, 2026
74082d2
minor review suggestions
carljm Feb 15, 2026
2e519b2
fixed broken tests
carljm Feb 16, 2026
8c3e4a5
go back to just flatten()
carljm Feb 16, 2026
3862436
rename Bindings::iter[_mut] to Bindings::iter_flat[_mut]
carljm Feb 16, 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 @@ -151,7 +151,7 @@ static FREQTRADE: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
600,
650,
);

static PANDAS: Benchmark = Benchmark::new(
Expand Down
139 changes: 139 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/async.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,142 @@ async def f():

reveal_type(f()) # revealed: CoroutineType[Any, Any, Unknown]
```

## Awaiting intersection types (3.13+)

```toml
[environment]
python-version = "3.13"
```

Intersection types can be awaited when their elements are awaitable. This is important for patterns
like `inspect.isawaitable()` which narrow types to intersections with `Awaitable`.

```py
import inspect
from typing import Any

def get_any() -> Any:
pass

async def test():
x = get_any()
if inspect.isawaitable(x):
reveal_type(x) # revealed: Any & Awaitable[object]
y = await x
reveal_type(y) # revealed: Any
```

The return type of awaiting an intersection is the intersection of the return types of awaiting each
element:

```py
from typing import Coroutine
from ty_extensions import Intersection

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

async def test(x: Intersection[Coroutine[object, object, A], Coroutine[object, object, B]]):
y = await x
reveal_type(y) # revealed: A & B
```

If some intersection elements are not awaitable, we skip them and use the return types from the
awaitable elements:

```py
from typing import Coroutine
from ty_extensions import Intersection

class NotAwaitable: ...

async def test(x: Intersection[Coroutine[object, object, str], NotAwaitable]):
y = await x
reveal_type(y) # revealed: str
```

When an intersection includes `Any`, awaiting succeeds for both elements. `Any` is awaitable and
returns `Any`:

```py
from typing import Coroutine, Any
from ty_extensions import Intersection

async def test(x: Intersection[Coroutine[object, object, int], Any]):
y = await x
reveal_type(y) # revealed: int & Any
```

When an intersection has three or more elements, some awaitable and some not, the non-awaitable
elements are skipped:

```py
from typing import Coroutine
from ty_extensions import Intersection

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

async def test(x: Intersection[Coroutine[object, object, A], Coroutine[object, object, B], NotAwaitable]):
y = await x
reveal_type(y) # revealed: A & B
```

If all intersection elements fail to be awaitable, the await is invalid:

```py
from ty_extensions import Intersection

class NotAwaitable1: ...
class NotAwaitable2: ...

async def test(x: Intersection[NotAwaitable1, NotAwaitable2]):
# error: [invalid-await]
await x
```

When a callable is narrowed with `TypeIs[Top[Callable[..., Awaitable[...]]]]`, the narrowed
intersection should contribute the top-callable return type to the call result, even though the
top-callable itself cannot be safely called.

```py
from typing import Awaitable, Callable
from typing_extensions import TypeIs
from ty_extensions import Top

def is_async_callable(x: object) -> TypeIs[Top[Callable[..., Awaitable[object]]]]:
return True

async def f(fn: Callable[[int], int | Awaitable[int]]) -> None:
if is_async_callable(fn):
reveal_type(fn) # revealed: ((int, /) -> int | Awaitable[int]) & Top[(...) -> Awaitable[object]]
result = fn(1)
# This includes `int & Awaitable[object]`: an `int` subtype could define `__await__`.
reveal_type(result) # revealed: (int & Awaitable[object]) | Awaitable[int]
reveal_type(await result) # revealed: object
```

## Awaiting intersection types (Python 3.12 or lower)

```toml
[environment]
python-version = "3.12"
```

The return type of awaiting an intersection is the intersection of the return types of awaiting each
element:

```py
from typing import Coroutine
from ty_extensions import Intersection

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

async def test(x: Intersection[Coroutine[object, object, A], Coroutine[object, object, B]]):
y = await x
# TODO: should be `A & B`, but suffers from https://github.com/astral-sh/ty/issues/2426
reveal_type(y) # revealed: A
```
73 changes: 73 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/call/union.md
Original file line number Diff line number Diff line change
Expand Up @@ -870,3 +870,76 @@ def _(flag: bool):
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `T`, found `dict[str, int] & dict[Unknown | str, Unknown | int]`"
f({"y": 1})
```

## Union of intersections with failing bindings

<!-- snapshot-diagnostics -->

When calling a union where one element is an intersection of callables, and all bindings in that
intersection fail, we should report errors with both union and intersection context.

```py
from ty_extensions import Intersection
from typing import Callable

class IntCaller:
def __call__(self, x: int) -> int:
return x

class StrCaller:
def __call__(self, x: str) -> str:
return x

class BytesCaller:
def __call__(self, x: bytes) -> bytes:
return x

def test(f: Intersection[IntCaller, StrCaller] | BytesCaller):
# Call with None - should fail for IntCaller, StrCaller, and BytesCaller
# error: [invalid-argument-type]
# error: [invalid-argument-type]
# error: [invalid-argument-type]
f(None)
```

## Union semantics with constrained callable typevars

```toml
[environment]
python-version = "3.13"
```

Calling through a union that includes a constrained callable `TypeVar` must preserve union
semantics: all possible callable variants of the constrained `TypeVar` still need to accept the
argument list.

```py
from typing import Callable

def test[T: (Callable[[int], int], Callable[[str], str])](
f: T | Callable[[int], int],
) -> None:
# `f` may be `Callable[[str], str]`, so this call is not safe.
# error: [invalid-argument-type]
f(1)
```

## Union semantics with callable aliases in outer unions

```toml
[environment]
python-version = "3.12"
```

The same issue appears when the nested union comes from a callable type alias:

```py
from typing import Callable

type Alias = Callable[[int], int] | Callable[[str], str]

def test_alias(f: Alias | Callable[[int], int]) -> None:
# `f` may be `Callable[[str], str]`, so this call is not safe.
# error: [invalid-argument-type]
f(1)
```
Original file line number Diff line number Diff line change
Expand Up @@ -1453,6 +1453,41 @@ class C:
reveal_type(C.__init__) # revealed: (self: C, field: str | int = ...) -> None
```

### Intersections of descriptor `__set__` types

When the descriptor type is an intersection, the generated `__init__` parameter should use the
intersection of the acceptable `value` types from `__set__`.

```py
from dataclasses import dataclass
from typing import Callable, cast
from ty_extensions import Intersection

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

def set_a(self: "DescA", instance: object, value: A) -> None: ...
def set_b(self: "DescB", instance: object, value: B) -> None: ...

class DescA:
# We use callable attributes instead of regular methods here because regular methods currently
# trigger a separate known issue where method attributes on intersections can collapse to `Never`:
# https://github.com/astral-sh/ty/issues/2428
__set__: Callable[["DescA", object, A], None] = set_a

class DescB:
__set__: Callable[["DescB", object, B], None] = set_b

@dataclass
class C:
field: Intersection[DescA, DescB] = cast(
Intersection[DescA, DescB],
DescA(),
)

reveal_type(C.__init__) # revealed: (self: C, field: A & B = ...) -> None
```

## `dataclasses.field`

To do
Expand Down
Loading
Loading