Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
134 changes: 103 additions & 31 deletions crates/ty_python_semantic/resources/mdtest/bidirectional.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ when generics are involved, the type of an outer expression can sometimes be use
inner expressions. Bidirectional type inference is a mechanism that propagates such "expected types"
to the inference of inner expressions.

## Propagating target type annotation

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

## Propagating target type annotation

```py
from typing import Literal

Expand Down Expand Up @@ -80,11 +80,6 @@ def _() -> TD:

## Propagating return type annotation

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

```py
from typing import overload, Callable

Expand Down Expand Up @@ -192,11 +187,6 @@ def f() -> list[Literal[1]]:

## Instance attributes

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

Both meta and class/instance attribute annotations are used as type context:

```py
Expand Down Expand Up @@ -240,13 +230,110 @@ def _(xy: X | Y):
xy.x = reveal_type([1]) # revealed: list[int]
```

## Class constructor parameters
## Overload evaluation

```toml
[environment]
python-version = "3.12"
The type context of all matching overloads are considered during argument inference:

```py
from typing import overload, TypedDict

def int_or_str() -> int | str:
raise NotImplementedError

@overload
def f1(x: list[int | None], y: int) -> int: ...
@overload
def f1(x: list[int | str], y: str) -> str: ...
def f1(x, y) -> int | str:
raise NotImplementedError

# TODO: We should reveal `list[int]` here.
x1 = f1(reveal_type([1]), 1) # revealed: list[int]
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The main remaining limitation of this PR is that we do not "choose" the inference of the matching overload after overload evaluation, so hover types will show types inferred without type context. This is also the behavior on main -- I'll try to address it in a follow up (but it doesn't seem very high priority).

reveal_type(x1) # revealed: int

x2 = f1(reveal_type([1]), int_or_str()) # revealed: list[int]
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 notice you don't have a TODO comment here -- but list[int] seems just as wrong here as above. I think it should be list[int | None] | list[int | str]?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

In the case that there is a single applicable type context (after overload resolution), we should reveal the type inferred with that type context. When there are multiple and we actually infer the type multiple times for each matching overload, it's less clear what to reveal here. It's closer to list[int | None] & list[int | str] than list[int | None] | list[int | str] (but this PR is about removing the intersection types after all). If we actually did infer a union, overload resolution would fail here, so I'm not sure revealing the union makes sense. pyright also reveals the type-contextless inference. We could maybe reveal a ty internal Both[list[int | None], list[int | str]], but that also seems confusing?

reveal_type(x2) # revealed: int | str

@overload
def f2[T](x: T, y: int) -> T: ...
@overload
def f2(x: list[int | str], y: str) -> object: ...
def f2(x, y) -> object: ...

x3 = f2(reveal_type([1]), 1) # revealed: list[int]
reveal_type(x3) # revealed: list[int]

class TD(TypedDict):
x: list[int | str]

class TD2(TypedDict):
x: list[int | None]

@overload
def f3(x: TD, y: int) -> int: ...
@overload
def f3(x: TD2, y: str) -> str: ...
def f3(x, y) -> object: ...

# TODO: We should reveal `TD2` here.
x4 = f3(reveal_type({"x": [1]}), "1") # revealed: dict[str, list[int]]
reveal_type(x4) # revealed: str

x5 = f3(reveal_type({"x": [1]}), int_or_str()) # revealed: dict[str, list[int]]
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.

Here again it seems like a TODO comment is warranted, as much as it is in the previous case?

reveal_type(x5) # revealed: int | str

@overload
def f4[T](_: list[T]) -> list[T]: ...
@overload
def f4(_: list[str]) -> list[str]: ...
def f4(_: object): ...

x6 = f4(reveal_type([])) # revealed: list[Unknown]
reveal_type(x6) # revealed: list[Unknown]

@overload
def f5(_: list[int | str]) -> int: ...
@overload
def f5(_: set[int | str]) -> str: ...
def f5(_) -> object:
raise NotImplementedError

def list_or_set[T](x: T) -> list[T] | set[T]:
raise NotImplementedError

# TODO: We should reveal `list[int | str] | set[int | str]` here.
x7 = f5(reveal_type(list_or_set(1))) # revealed: list[int] | set[int]
reveal_type(x7) # revealed: int | str

@overload
def f6(_: list[int | None]) -> int: ...
@overload
def f6(_: set[int | str]) -> str: ...
def f6(_) -> object:
raise NotImplementedError

def list_or_set2[T, U](x: T, y: U) -> list[T] | set[U]:
raise NotImplementedError

# TODO: We should not error here.
# error: [no-matching-overload]
x8 = f6(reveal_type(list_or_set2(1, 1))) # revealed: list[int] | set[int]
Copy link
Copy Markdown
Member Author

@ibraheemdev ibraheemdev Mar 13, 2026

Choose a reason for hiding this comment

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

Note that pyright does not handle this case correctly either, and it seems very tricky to implement correctly (and also very unlikely to occur in practice). pyright is also not able to resolve f5 above, which we are able to.

reveal_type(x8) # revealed: Unknown

@overload
def f7(y: list[int | str]) -> list[int | str]: ...
@overload
def f7[T](y: list[T]) -> list[T]: ...
def f7(y: object) -> object:
raise NotImplementedError

# TODO: We should reveal `list[int | str]` here.
x9 = f7(reveal_type(["Sheet1"])) # revealed: list[str]
reveal_type(x9) # revealed: list[int | str]
```

## Class constructor parameters

The parameters of both `__init__` and `__new__` are used as type context sources for constructor
calls:

Expand All @@ -269,11 +356,6 @@ A(f([]))

## Conditional expressions

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

The type context is propagated through both branches of conditional expressions:

```py
Expand All @@ -290,11 +372,6 @@ def _(flag: bool):

## Dunder Calls

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

The key and value parameters types are used as type context for `__setitem__` dunder calls:

```py
Expand Down Expand Up @@ -387,11 +464,6 @@ def _(x: Intersection[X, Y]):

## Multi-inference diagnostics

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

Diagnostics unrelated to the type-context are only reported once:

```py
Expand Down
8 changes: 8 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/typed_dict.md
Original file line number Diff line number Diff line change
Expand Up @@ -1566,10 +1566,14 @@ config["port"] = 80
from typing import TypedDict
from typing_extensions import NotRequired

class Inner(TypedDict):
inner: int

class Person(TypedDict):
name: str
age: int | None
extra: NotRequired[str]
inner: NotRequired[Inner]

def _(p: Person) -> None:
reveal_type(p.keys()) # revealed: dict_keys[str, object]
Expand All @@ -1590,6 +1594,10 @@ def _(p: Person) -> None:
# The type of the default parameter can be anything:
reveal_type(p.get("extra", 0)) # revealed: str | Literal[0]

# Even another typed dict:
# TODO: This should evaluate to `Inner`.
reveal_type(p.get("inner", {"inner": 0})) # revealed: Inner | dict[str, int]

# We allow access to unknown keys (they could be set for a subtype of Person)
reveal_type(p.get("unknown")) # revealed: Unknown | None
reveal_type(p.get("unknown", "default")) # revealed: Unknown | Literal["default"]
Expand Down
Loading
Loading