crates/ty_python_semantic/resources/mdtest/bidirectional.md
ty partially supports bidirectional type inference. This is a mechanism for inferring the type of an expression "from the outside in". Normally, type inference proceeds "from the inside out". That is, in order to infer the type of an expression, the types of all sub-expressions must first be inferred. There is no reverse dependency. However, when performing complex type inference, such as when generics are involved, the type of an outer expression can sometimes be useful in inferring inner expressions. Bidirectional type inference is a mechanism that propagates such "expected types" to the inference of inner expressions.
[environment]
python-version = "3.13"
Type context is sourced from various places, including annotated assignments:
from typing import Literal
a: list[Literal[1]] = [1]
Function parameter annotations:
def b(x: list[Literal[1]]): ...
b([1])
Bound method parameter annotations:
class C:
def __init__(self, x: list[Literal[1]]): ...
def foo(self, x: list[Literal[1]]): ...
C([1]).foo([1])
Declared variable types:
d: list[Literal[1]]
d = [1]
Declared attribute types:
class E:
a: list[Literal[1]]
b: list[Literal[1]]
def _(e: E):
e.a = [1]
E.b = [1]
Function return types:
def f() -> list[Literal[1]]:
return [1]
import typing
a: list[int] = [1, 2, 3]
reveal_type(a) # revealed: list[int]
b: list[int | str] = [1, 2, 3]
reveal_type(b) # revealed: list[int | str]
c: typing.List[int] = [1, 2, 3]
reveal_type(c) # revealed: list[int]
d: list[typing.Any] = []
reveal_type(d) # revealed: list[Any]
e: set[int] = {1, 2, 3}
reveal_type(e) # revealed: set[int]
f: set[int | str] = {1, 2, 3}
reveal_type(f) # revealed: set[int | str]
g: typing.Set[int] = {1, 2, 3}
reveal_type(g) # revealed: set[int]
h: list[list[int]] = [[], [42]]
reveal_type(h) # revealed: list[list[int]]
i: list[typing.Any] = [1, 2, "3", ([4],)]
reveal_type(i) # revealed: list[Any]
j: list[tuple[str | int, ...]] = [(1, 2), ("foo", "bar"), ()]
reveal_type(j) # revealed: list[tuple[str | int, ...]]
k: list[tuple[list[int], ...]] = [([],), ([1, 2], [3, 4]), ([5], [6], [7])]
reveal_type(k) # revealed: list[tuple[list[int], ...]]
l: tuple[list[int], *tuple[list[typing.Any], ...], list[str]] = ([1, 2, 3], [4, 5, 6], [7, 8, 9], ["10", "11", "12"])
reveal_type(l) # revealed: tuple[list[int], list[Any], list[Any], list[str]]
type IntList = list[int]
m: IntList = [1, 2, 3]
reveal_type(m) # revealed: list[int]
n: list[typing.Literal[1, 2, 3]] = [1, 2, 3]
reveal_type(n) # revealed: list[Literal[1, 2, 3]]
o: list[typing.LiteralString] = ["a", "b", "c"]
reveal_type(o) # revealed: list[LiteralString]
p: dict[int, int] = {}
reveal_type(p) # revealed: dict[int, int]
q: dict[int | str, int] = {1: 1, 2: 2, 3: 3}
reveal_type(q) # revealed: dict[int | str, int]
r: dict[int | str, int | str] = {1: 1, 2: 2, 3: 3}
reveal_type(r) # revealed: dict[int | str, int | str]
s: dict[int | str, int | str]
s = {1: 1, 2: 2, 3: 3}
reveal_type(s) # revealed: dict[int | str, int | str]
(s := {1: 1, 2: 2, 3: 3})
reveal_type(s) # revealed: dict[int | str, int | str]
import typing
a: list[int] | None = [1, 2, 3]
reveal_type(a) # revealed: list[int]
b: list[int | str] | None = [1, 2, 3]
reveal_type(b) # revealed: list[int | str]
c: typing.List[int] | None = [1, 2, 3]
reveal_type(c) # revealed: list[int]
d: list[typing.Any] | None = []
reveal_type(d) # revealed: list[Any]
e: set[int] | None = {1, 2, 3}
reveal_type(e) # revealed: set[int]
f: set[int | str] | None = {1, 2, 3}
reveal_type(f) # revealed: set[int | str]
g: typing.Set[int] | None = {1, 2, 3}
reveal_type(g) # revealed: set[int]
h: list[list[int]] | None = [[], [42]]
reveal_type(h) # revealed: list[list[int]]
i: list[typing.Any] | None = [1, 2, "3", ([4],)]
reveal_type(i) # revealed: list[Any]
j: list[tuple[str | int, ...]] | None = [(1, 2), ("foo", "bar"), ()]
reveal_type(j) # revealed: list[tuple[str | int, ...]]
k: list[tuple[list[int], ...]] | None = [([],), ([1, 2], [3, 4]), ([5], [6], [7])]
reveal_type(k) # revealed: list[tuple[list[int], ...]]
l: tuple[list[int], *tuple[list[typing.Any], ...], list[str]] | None = ([1, 2, 3], [4, 5, 6], [7, 8, 9], ["10", "11", "12"])
reveal_type(l) # revealed: tuple[list[int], list[Any], list[Any], list[str]]
type IntList = list[int]
m: IntList | None = [1, 2, 3]
reveal_type(m) # revealed: list[int]
n: list[typing.Literal[1, 2, 3]] | None = [1, 2, 3]
reveal_type(n) # revealed: list[Literal[1, 2, 3]]
o: list[typing.LiteralString] | None = ["a", "b", "c"]
reveal_type(o) # revealed: list[LiteralString]
p: dict[int, int] | None = {}
reveal_type(p) # revealed: dict[int, int]
q: dict[int | str, int] | None = {1: 1, 2: 2, 3: 3}
reveal_type(q) # revealed: dict[int | str, int]
r: dict[int | str, int | str] | None = {1: 1, 2: 2, 3: 3}
reveal_type(r) # revealed: dict[int | str, int | str]
from typing import Any
x1: list[int] = [1, 2, *(3, 4, 5)]
reveal_type(x1) # revealed: list[int]
x2: list[list[int]] = [[1], [2], *([3], [4])]
reveal_type(x2) # revealed: list[list[int]]
x3: dict[str, int] = {"a": 1, **{"b": 2}}
reveal_type(x3) # revealed: dict[str, int]
def dynamic_mapping() -> Any: ...
x4: dict[str, int] = reveal_type({**dynamic_mapping()}) # revealed: dict[str | Any, int | Any]
# error: [invalid-argument-type] "Argument expression after ** must be a mapping type"
x5: dict[str, int] = {**42}
from collections.abc import Mapping, Sequence
from typing import Literal
x1: list[int | str] | list[int | None] = [1, 2, 3]
reveal_type(x1) # revealed: list[int | str]
x2: Sequence[int | str] | Sequence[int | None] = [1, 2, 3]
reveal_type(x2) # revealed: list[int]
x3: list[int] | list[int | None] | list[str | None] = ["1", "2"]
reveal_type(x3) # revealed: list[str | None]
x4: dict[str, list[int | None]] | dict[str, list[str | None]] = {"a": ["b"]}
reveal_type(x4) # revealed: dict[str, list[str | None]]
x5: Mapping[str, list[int | None]] | Mapping[str, list[str | None]] = {"a": ["b"]}
reveal_type(x5) # revealed: dict[str, list[str | None]]
def _(x6: list[dict[str, list[int] | int] | dict[str, list[int]]]):
x6.append(reveal_type({"b": 1})) # revealed: dict[str, list[int] | int]
type EitherList = list[int | str] | list[int | None]
x7: EitherList = [None, None]
reveal_type(x7) # revealed: list[int | None]
x8: EitherList = ["1", "2", "3"]
reveal_type(x8) # revealed: list[int | str]
type SelfOp[T] = Mapping[Literal["$eq", "$ne"], T]
type ListOp[T] = Mapping[Literal["$in", "$nin"], Sequence[T]]
type Ops[T] = SelfOp[T] | ListOp[T]
type NestedOp[T] = T | Ops[T]
x9: NestedOp[str] = {"$in": ["a", "b"]}
reveal_type(x9) # revealed: dict[Literal["$in", "$nin"], list[str]]
def singleton[T](x: T) -> list[T]:
return [x]
x1: list[list[int | str]] = [[1], [2]] * 3
reveal_type(x1) # revealed: list[list[int | str]]
x2: list[list[int | str]] = 3 * ([[1]] + [[2]])
reveal_type(x2) # revealed: list[list[int | str]]
x3: list[int | str] = 3 * ["x" for _ in range(3)]
reveal_type(x3) # revealed: list[int | str]
x4: set[int | str] = {1, 2} | {3, 4}
reveal_type(x4) # revealed: set[int | str]
x5: dict[int | str, int | str] = {1: 2} | {3: 4}
reveal_type(x5) # revealed: dict[int | str, int | str]
# TODO: We currently eagerly pass type context to collection literals on either side of a binary
# operator. That makes the cases above work, but it is not generally sound.
class X:
def __add__(self, _: list[int]) -> list[int | str]:
return []
# error: [unsupported-operator] "Operator `+` is not supported between objects of type `X` and `list[int | str]`"
x6: list[int | str] = X() + [1]
# TODO: We do not yet propagate type context through the generic call.
# error: [invalid-assignment] "Object of type `list[int]` is not assignable to `list[int | str]`"
x7: list[int | str] = singleton(42) * 3
x1: set[int | str] = {42 for _ in range(3)}
reveal_type(x1) # revealed: set[int | str]
x2: dict[int | str, int | str] = {str(i): i for i in range(3)}
reveal_type(x2) # revealed: dict[int | str, int | str]
from typing import Literal
def singleton[T](x: T) -> list[T]:
return [x]
# Tuple elements are inferred individually, but type context can prevent e.g. `int` widening.
x1: tuple[list[Literal[1]]] = (singleton(1),)
reveal_type(x1) # revealed: tuple[list[Literal[1]]]
x2: tuple[list[Literal[1]], ...] = (singleton(1),) * 3
reveal_type(x2) # revealed: tuple[list[Literal[1]], ...]
x3: tuple[list[Literal[1]], ...] = 3 * ((singleton(1),) + (singleton(1),))
reveal_type(x3) # revealed: tuple[list[Literal[1]], ...]
from collections.abc import AsyncGenerator, AsyncIterable, Generator, Iterable
class TextContent: ...
class TagContent: ...
def expects_generator_content(content: Generator[list[TextContent | TagContent], None, None]) -> None: ...
def expects_iterable_content(content: Iterable[list[TextContent | TagContent]]) -> None: ...
def expects_optional_iterable_content(content: Iterable[list[TextContent | TagContent]] | None) -> None: ...
def generator_content() -> None:
expects_generator_content([TextContent()] for _ in range(1))
expects_iterable_content([TextContent()] for _ in range(1))
expects_optional_iterable_content([TextContent()] for _ in range(1))
expects_generator_content((reveal_type([TextContent()]) for _ in range(1))) # revealed: list[TextContent | TagContent]
def expects_int_iterable_or_str_generator(content: Generator[list[str], int, None] | Iterable[list[int]]) -> None: ...
def generator_content_with_incompatible_generator_arm() -> None:
expects_int_iterable_or_str_generator((reveal_type([]) for _ in range(1))) # revealed: list[int]
def invalid_generator_content() -> None:
expects_generator_content([object()] for _ in range(1)) # error: [invalid-argument-type]
expects_optional_iterable_content([object()] for _ in range(1)) # error: [invalid-argument-type]
async def async_texts() -> AsyncGenerator[TextContent, None]:
yield TextContent()
def expects_async_generator_content(content: AsyncGenerator[list[TextContent | TagContent], None]) -> None: ...
def expects_async_iterable_content(content: AsyncIterable[list[TextContent | TagContent]]) -> None: ...
async def async_generator_content() -> None:
expects_async_generator_content([TextContent()] async for _ in async_texts())
expects_async_iterable_content([TextContent()] async for _ in async_texts())
async def invalid_async_generator_content() -> None:
expects_async_generator_content([object()] async for _ in async_texts()) # error: [invalid-argument-type]
The declared type of a generic call expression is used to infer a more assignable specialization for the callable:
from typing import Literal
def f[T](x: T) -> list[T]:
return [x]
x1 = f("a")
reveal_type(x1) # revealed: list[str]
x2: list[int | Literal["a"]] = f("a")
reveal_type(x2) # revealed: list[int | Literal["a"]]
x3: list[int | str] = f("a")
reveal_type(x3) # revealed: list[int | str]
x4: list[int | tuple[int, int]] = f((1, 2))
reveal_type(x4) # revealed: list[int | tuple[int, int]]
x5: list[int] = f(True)
reveal_type(x5) # revealed: list[int]
# error: [invalid-assignment] "Object of type `list[str]` is not assignable to `list[int]`"
x6: list[int] = f("a")
# error: [invalid-assignment] "Object of type `list[str]` is not assignable to `tuple[int]`"
x7: tuple[int] = f("a")
def f2[T: int](x: T) -> T:
return x
x8: int = f2(True)
reveal_type(x8) # revealed: Literal[True]
x9: int | str = f2(True)
reveal_type(x9) # revealed: Literal[True]
from typing import Callable, overload
def singleton[T](x: T) -> list[T]:
return [x]
x10: list[int | str] | None = singleton(1)
reveal_type(x10) # revealed: list[int | str]
def value_or_list[T](x: T, cond: bool) -> T | list[T]:
return x if cond else [x]
x11: int | list[int] = value_or_list(1, True)
reveal_type(x11) # revealed: int | list[int]
def returns_objects() -> list[object]:
reveal_type(singleton(1)) # revealed: list[int]
# `list[int]` and `list[object]` are incompatible, but the return type check passes because
# this call is inferred using the annotated return type.
return singleton(1)
def returns_optional_objects() -> list[object] | None:
return singleton(1)
def deco[T](func: Callable[[], T]) -> Callable[[], T]:
return func
def outer() -> Callable[[], list[object]]:
@deco
def inner() -> list[object]:
return singleton(1)
return inner
@overload
def overloaded(x: int) -> list[int]: ...
@overload
def overloaded(x: str) -> list[str]: ...
def overloaded(x: int | str) -> list[int] | list[str]:
# `list[int] | list[str]` is disjoint from `list[int | str]`.
if isinstance(x, int):
return singleton(x)
else:
return singleton(x)
reveal_type(overloaded(1)) # revealed: list[int]
reveal_type(overloaded("a")) # revealed: list[str]
async def async_return() -> list[int | str]:
return singleton(1)
def forward[T](x: T, cond: bool) -> T | list[T]:
return forwarded(x, cond)
def forwarded[T](x: T, cond: bool) -> T | list[T]:
return x if cond else [x]
The same applies to constructors of generic classes:
from typing import Any
class X[T]:
def __init__(self, value: T):
self.value = value
x1: X[int] = X(1)
reveal_type(x1) # revealed: X[int]
x2: X[int | None] = X(1)
reveal_type(x2) # revealed: X[int | None]
x3: X[int | None] | None = X(1)
reveal_type(x3) # revealed: X[int | None]
def _[T](x1: X[T]):
x2: X[T | int] = X(x1.value)
reveal_type(x2) # revealed: X[T@_ | int]
x4: X[Any] = X(1)
reveal_type(x4) # revealed: X[Any]
def _(flag: bool):
x5: X[int | None] = X(1) if flag else X(2)
reveal_type(x5) # revealed: X[int | None]
from dataclasses import dataclass
@dataclass
class Y[T]:
value: T
y1 = Y(value=1)
reveal_type(y1) # revealed: Y[int]
y2: Y[Any] = Y(value=1)
reveal_type(y2) # revealed: Y[Any]
class Z[T]:
value: T
def __new__(cls, value: T):
return super().__new__(cls)
z1 = Z(1)
reveal_type(z1) # revealed: Z[int]
z2: Z[Any] = Z(1)
reveal_type(z2) # revealed: Z[Any]
The return type should preserve the independent key and value types of a generic dict constructor:
from collections.abc import Iterable, Mapping
def dict_with_numeric_promotion(
keys: Iterable[float],
values: Iterable[int],
) -> Mapping[float, int]:
return dict(zip(keys, values))
from collections.abc import Callable, Hashable
from typing import Any
# The `dict(...)` variant is not technically allowed by the typeshed overloads, which require
# string keys for keyword arguments. We special-case it to match the literal form.
x1: dict[Hashable, Callable[..., object]] = {"x": lambda: 1}
x2: dict[Hashable, Callable[..., object]] = dict(x=lambda: 1)
A function's arguments are also inferred using the type context:
from typing import Callable, TypedDict
class TD(TypedDict):
x: int
def first[T](x: list[T]) -> T:
return x[0]
type ObjectCallback = Callable[[object], None]
type IntCallback = Callable[[int], None]
def make_callback[T](callback: Callable[[T], None]) -> Callable[[T], None]:
return callback
def consume(value: int) -> None:
pass
x1: TD = first([{"x": 0}, {"x": 1}])
reveal_type(x1) # revealed: TD
x2: TD | None = first([{"x": 0}, {"x": 1}])
reveal_type(x2) # revealed: TD
# error: [missing-typed-dict-key] "Missing required key 'x' in TypedDict `TD` constructor"
# error: [invalid-key] "Unknown key "y" for TypedDict `TD`"
# error: [invalid-assignment] "Object of type `TD | dict[str, int]` is not assignable to `TD`"
x3: TD = first([{"y": 0}, {"x": 1}])
# error: [missing-typed-dict-key] "Missing required key 'x' in TypedDict `TD` constructor"
# error: [invalid-key] "Unknown key "y" for TypedDict `TD`"
# error: [invalid-assignment] "Object of type `TD | None | dict[str, int]` is not assignable to `TD | None`"
x4: TD | None = first([{"y": 0}, {"x": 1}])
# `ObjectCallback` is redundant in this union, so expanding the aliases collapses the narrowing
# target to `IntCallback`.
x5: ObjectCallback | IntCallback = make_callback(lambda value: consume(value.bit_length()))
But not in a way that leads to assignability errors:
from typing import Any, Sequence, TypedDict
class TD2(TypedDict):
x: str
def _(dt: dict[str, Any], key: str):
x1: TD = dt.get(key, {})
reveal_type(x1) # revealed: TD
x2: TD = dt.get(key, {"x": 0})
reveal_type(x2) # revealed: TD
x3: TD | None = dt.get(key, {})
reveal_type(x3) # revealed: TD | None
x4: TD | None = dt.get(key, {"x": 0})
reveal_type(x4) # revealed: TD | None
x5: TD2 = dt.get(key, {})
reveal_type(x5) # revealed: TD2
x6: TD2 = dt.get(key, {"x": 0})
reveal_type(x6) # revealed: TD2
x7: TD2 | None = dt.get(key, {})
reveal_type(x7) # revealed: TD2 | None
x8: TD2 | None = dt.get(key, {"x": 0})
reveal_type(x8) # revealed: TD2 | None
def as_sequence[T](x: T, y: list[T], z: list[T]) -> Sequence[T]:
return [x]
def _(x: int, z: list[int]):
x1: Sequence[int] = as_sequence(x, [x], z)
# TODO: A covariant type context should not cause us to unnecessarily widen call arguments.
x2: Sequence[int | str] = as_sequence(x, [x], z) # error: [invalid-argument-type]
Partially specialized type context is not ignored:
from typing import TypeVar
U = TypeVar("U", default=Any)
class X: ...
def lst[T](x: T) -> list[T]:
return [x]
def two_lists[T](x: list[T | int], y: list[T | str]) -> T:
raise NotImplementedError
def two_lists_default(x: list[U | int], y: list[U | str]) -> U:
raise NotImplementedError
def dct[K, V](k: K, v: V) -> dict[K, V]:
return {k: v}
def two_dicts[T](x: dict[T | int, Any], y: dict[T | str, Any]) -> T:
raise NotImplementedError
def two_dicts_default(x: dict[U | int, Any], y: dict[U | str, Any]) -> U:
raise NotImplementedError
def _():
# revealed: list[X | int]
# revealed: list[X | str]
x1 = two_lists(reveal_type(lst(X())), reveal_type(lst(X())))
reveal_type(x1) # revealed: X
# revealed: list[X | int]
# revealed: list[X | str]
x2 = two_lists(reveal_type([X()]), reveal_type([X()]))
reveal_type(x2) # revealed: X
# revealed: list[X | int]
# revealed: list[X | str]
x3 = two_lists_default(reveal_type(lst(X())), reveal_type(lst(X())))
reveal_type(x3) # revealed: X
# revealed: list[X | int]
# revealed: list[X | str]
x4 = two_lists_default(reveal_type([X()]), reveal_type([X()]))
reveal_type(x4) # revealed: X
# revealed: dict[X | int, Any]
# revealed: dict[X | str, Any]
x5 = two_dicts(reveal_type(dct(X(), X())), reveal_type(dct(X(), X())))
reveal_type(x5) # revealed: X
# revealed: dict[X | int, Any]
# revealed: dict[X | str, Any]
x6 = two_dicts(reveal_type({X(): X()}), reveal_type({X(): X()}))
reveal_type(x6) # revealed: X
# revealed: dict[X | int, Any]
# revealed: dict[X | str, Any]
x7 = two_dicts_default(reveal_type(dct(X(), X())), reveal_type(dct(X(), X())))
reveal_type(x7) # revealed: X
# revealed: dict[X | int, Any]
# revealed: dict[X | str, Any]
x8 = two_dicts_default(reveal_type({X(): X()}), reveal_type({X(): X()}))
reveal_type(x8) # revealed: X
When inferring a generic call, we only use the declared type as type context if it is in non-covariant position. The final annotated assignment binding still uses the declared type if the inferred and declared types are mutually assignable:
from typing import Any
class Bivariant[T]:
pass
class Covariant[T]:
def pop(self) -> T:
raise NotImplementedError
class Contravariant[T]:
def push(self, value: T) -> None:
pass
class Invariant[T]:
x: T
def bivariant[T](x: T) -> Bivariant[T]:
return Bivariant()
def covariant[T](x: T) -> Covariant[T]:
return Covariant()
def contravariant[T](x: T) -> Contravariant[T]:
return Contravariant()
def invariant[T](x: T) -> Invariant[T]:
return Invariant()
x1 = bivariant(1)
x2 = covariant(1)
x3 = contravariant(1)
x4 = invariant(1)
reveal_type(x1) # revealed: Bivariant[Literal[1]]
reveal_type(x2) # revealed: Covariant[Literal[1]]
reveal_type(x3) # revealed: Contravariant[int]
reveal_type(x4) # revealed: Invariant[int]
x5: Bivariant[int | None] = bivariant(1)
x6: Covariant[int | None] = covariant(1)
x7: Contravariant[int | None] = contravariant(1)
x8: Invariant[int | None] = invariant(1)
reveal_type(x5) # revealed: Bivariant[int | None]
reveal_type(x6) # revealed: Covariant[Literal[1]]
reveal_type(x7) # revealed: Contravariant[int | None]
reveal_type(x8) # revealed: Invariant[int | None]
x9: Bivariant[Any] = bivariant(1)
x10: Covariant[Any] = covariant(1)
x11: Contravariant[Any] = contravariant(1)
x12: Invariant[Any] = invariant(1)
reveal_type(x9) # revealed: Bivariant[Any]
reveal_type(x10) # revealed: Covariant[Any]
reveal_type(x11) # revealed: Contravariant[Any]
reveal_type(x12) # revealed: Invariant[Any]
This behavior also applies to invariant collection types:
from typing import Any
def f[T](x: T) -> list[T]:
return [x]
def f2[T](x: T) -> list[T] | None:
return [x]
def f3[T](x: T) -> list[T] | dict[T, T]:
return [x]
x1 = f(1)
reveal_type(x1) # revealed: list[int]
x2: list[Any] = f(1)
reveal_type(x2) # revealed: list[Any]
x3: list[Any] = [1]
reveal_type(x3) # revealed: list[Any]
x4: list[Any] | None = f(1)
reveal_type(x4) # revealed: list[Any]
x5: list[Any] | None = [1]
reveal_type(x5) # revealed: list[Any]
x6: list[Any] | None = f2(1)
reveal_type(x6) # revealed: list[Any] | None
x7: list[Any] | dict[Any, Any] = f3(1)
reveal_type(x7) # revealed: list[Any] | dict[Any, Any]
As well as constructors of generic classes:
class X[T]:
def __init__(self: "X[None]"): ...
def pop(self) -> T:
raise NotImplementedError
x1: X[int | None] = X()
reveal_type(x1) # revealed: X[None]
We also prefer the declared type of Callable parameters, which are in contravariant position:
from typing import Callable
type AnyToBool = Callable[[Any], bool]
def wrap[**P, T](f: Callable[P, T]) -> Callable[P, T]:
return f
def make_callable[T](x: T) -> Callable[[T], bool]:
raise NotImplementedError
def maybe_make_callable[T](x: T) -> Callable[[T], bool] | None:
raise NotImplementedError
x1: Callable[[Any], bool] = make_callable(0)
reveal_type(x1) # revealed: (Any, /) -> bool
x2: AnyToBool = make_callable(0)
reveal_type(x2) # revealed: (Any, /) -> bool
x3: Callable[[list[Any]], bool] = make_callable([0])
reveal_type(x3) # revealed: (list[Any], /) -> bool
x4: Callable[[Any], bool] = wrap(make_callable(0))
reveal_type(x4) # revealed: (Any, /) -> bool
x5: Callable[[Any], bool] | None = maybe_make_callable(0)
reveal_type(x5) # revealed: ((Any, /) -> bool) | None
Additionally, if the inferred type is a subtype of the declared type, we prefer declared type assignments that are in non-covariant position. This behavior applies to collection literals:
import builtins
from collections import defaultdict
from collections.abc import Mapping
from typing import Any, Callable, Iterable, Literal, MutableSequence, overload, Sequence
x1: Sequence[Any] = [1, 2, 3]
reveal_type(x1) # revealed: list[int]
x2: MutableSequence[Any] = [1, 2, 3]
reveal_type(x2) # revealed: list[Any]
x3: Iterable[Any] = [1, 2, 3]
reveal_type(x3) # revealed: list[int]
x4: Iterable[Iterable[Any]] = [[1, 2, 3]]
reveal_type(x4) # revealed: list[list[int]]
x5: list[Iterable[Any]] = [[1, 2, 3]]
reveal_type(x5) # revealed: list[Iterable[Any]]
x6: Iterable[list[Any]] = [[1, 2, 3]]
reveal_type(x6) # revealed: list[list[Any]]
x7: Sequence[Any] = [i for i in [1, 2, 3]]
reveal_type(x7) # revealed: list[int]
x8: MutableSequence[Any] = [i for i in [1, 2, 3]]
reveal_type(x8) # revealed: list[Any]
x9: Iterable[Any] = [i for i in [1, 2, 3]]
reveal_type(x9) # revealed: list[int]
x10: Iterable[Iterable[Any]] = [[i] for i in [1, 2, 3]]
reveal_type(x10) # revealed: list[list[int]]
x11: list[Iterable[Any]] = [[i] for i in [1, 2, 3]]
reveal_type(x11) # revealed: list[Iterable[Any]]
x12: Iterable[list[Any]] = [[i] for i in [1, 2, 3]]
reveal_type(x12) # revealed: list[list[Any]]
As well as generic calls, and constructors of generic classes:
class X[T]:
value: T
def __init__(self, value: T): ...
class A[T](X[T]): ...
def a[T](value: T) -> A[T]:
return A(value)
x13: A[object] = A(1)
reveal_type(x13) # revealed: A[object]
x14: X[object] = A(1)
reveal_type(x14) # revealed: A[object]
x15: X[object] | None = A(1)
reveal_type(x15) # revealed: A[object]
x16: X[object] | None = a(1)
reveal_type(x16) # revealed: A[object]
def f[T](x: T) -> list[list[T]]:
return [[x]]
x17: Sequence[Sequence[Any]] = f(1)
reveal_type(x17) # revealed: list[list[int]]
x18: Sequence[list[Any]] = f(1)
reveal_type(x18) # revealed: list[list[Any]]
x19: dict[int, dict[str, int]] = defaultdict(dict)
reveal_type(x19) # revealed: defaultdict[int, dict[str, int]]
Complex subtyping relationships are solved correctly:
from typing import Hashable
def variadic(*args: Any, **kwargs: Any) -> Any: ...
x20: Mapping[Hashable, list[Callable[..., Any]]] = {"x": [variadic]}
reveal_type(x20) # revealed: dict[Hashable, list[(...) -> Any]]
x21: Mapping[Hashable, list[Callable[..., Any]]] = dict(x=[variadic])
reveal_type(x21) # revealed: dict[Hashable, list[(...) -> Any]]
Callable type context is also used to inform the implicit specialization of a generic class:
import builtins
from collections import defaultdict
from collections.abc import Mapping
from typing import Any, Callable, overload
x1: Mapping[str, list[str]] = reveal_type(defaultdict(list)) # revealed: defaultdict[str, list[str]]
x1["key"].append(1) # error: [invalid-argument-type]
x2: Callable[[], list[str]] = reveal_type(list) # revealed: <class 'list[str]'>
reveal_type(x2()) # revealed: list[str]
x3: Callable[[], list[str]] | None = reveal_type(list) # revealed: <class 'list[str]'>
x4: Callable[[], list[str]] = reveal_type(builtins.list) # revealed: <class 'list[str]'>
reveal_type(x4()) # revealed: list[str]
type ListFactory = Callable[[], list[str]]
x5: ListFactory = reveal_type(list) # revealed: <class 'list[str]'>
reveal_type(x5()) # revealed: list[str]
x6: Callable[..., Any] = reveal_type(list) # revealed: <class 'list'>
x7: Callable[[Any], Any] = reveal_type(list) # revealed: <class 'list'>
class Wrapped[T]:
value: T
def __new__(cls, value: T) -> "Wrapped[tuple[T]]":
raise NotImplementedError
x8: Callable[[str], Wrapped[tuple[str]]] = reveal_type(Wrapped) # revealed: <class 'Wrapped[str]'>
reveal_type(x8("x")) # revealed: Wrapped[tuple[str]]
class M[T]:
value: T
def __new__[S](cls, value: S) -> "M[tuple[S]]":
raise NotImplementedError
x9: Callable[[str], M[tuple[str]]] = reveal_type(M) # revealed: <class 'M'>
reveal_type(x9("x")) # revealed: M[tuple[str]]
class MultiPath[T]:
value: T
@overload
def __init__(self, value: T) -> None: ...
@overload
def __init__(self, value: list[T]) -> None: ...
def __init__(self, value: object) -> None: ...
# fmt: off
x10: Callable[[list[int]], MultiPath[int] | MultiPath[list[int]]] = reveal_type(MultiPath) # revealed: <class 'MultiPath'>
# fmt: on
When a generic call is checked against a union declared type, the union is narrowed to the first compatible element:
from typing import reveal_type, Any, Callable, TypedDict
def identity[T](x: T) -> T:
return x
type Target = Any | list[str] | dict[str, str] | Callable[[str], None] | None
def _(narrow: dict[str, str], target: Target):
target = identity(narrow)
reveal_type(target) # revealed: dict[str, str]
def _(narrow: list[str], target: Target):
target = identity(narrow)
reveal_type(target) # revealed: list[str]
def _(narrow: Callable[[str], None], target: Target):
target = identity(narrow)
reveal_type(target) # revealed: (str, /) -> None
def _(narrow: list[str] | dict[str, str], target: Target):
target = identity(narrow)
reveal_type(target) # revealed: list[str] | dict[str, str]
class TD(TypedDict):
x: int
type TargetWithTD = Any | list[TD] | dict[str, TD] | Callable[[TD], None] | None
def _(target: TargetWithTD):
target = identity([{"x": 1}])
reveal_type(target) # revealed: list[TD]
def _(target: TargetWithTD):
target = identity({"x": {"x": 1}})
reveal_type(target) # revealed: dict[str, TD]
def _(target: TargetWithTD):
def make_callable[T](x: T) -> Callable[[T], None]:
raise NotImplementedError
target = identity(make_callable({"x": 1}))
reveal_type(target) # revealed: (TD, /) -> None
def identity[T](x: T) -> T:
return x
def lst[T](x: T) -> list[T]:
return [x]
def _(i: int):
x1: int | None = i
x2: int | None = identity(i)
x3: int | str | None = identity(i)
reveal_type(x1) # revealed: int
reveal_type(x2) # revealed: int
reveal_type(x3) # revealed: int
x1: list[int | None] | None = [i]
x2: list[int | None] | None = identity([i])
x3: list[int | None] | int | None = identity([i])
reveal_type(x1) # revealed: list[int | None]
reveal_type(x2) # revealed: list[int | None]
reveal_type(x3) # revealed: list[int | None]
x1: list[int | None] | None = [i]
x2: list[int | None] | None = lst(i)
x3: list[int | None] | int | None = lst(i)
reveal_type(x1) # revealed: list[int | None]
reveal_type(x2) # revealed: list[int | None]
reveal_type(x3) # revealed: list[int | None]
x1: list | None = [] # error: [missing-type-argument]
x2: list | None = identity([]) # error: [missing-type-argument]
x3: list | int | None = identity([]) # error: [missing-type-argument]
reveal_type(x1) # revealed: list[Unknown]
reveal_type(x2) # revealed: list[Unknown]
reveal_type(x3) # revealed: list[Unknown]
def f[T](x: list[T]) -> T:
return x[0]
def _(a: int, b: str, c: int | str):
x1: int = f(lst(a))
reveal_type(x1) # revealed: int
x2: int | str = f(lst(a))
reveal_type(x2) # revealed: int
x3: int | None = f(lst(a))
reveal_type(x3) # revealed: int
x4: str = f(lst(b))
reveal_type(x4) # revealed: str
x5: int | str = f(lst(b))
reveal_type(x5) # revealed: str
x6: str | int = f(lst(b))
reveal_type(x6) # revealed: str
x7: str | None = f(lst(b))
reveal_type(x7) # revealed: str
x8: int | str = f(lst(c))
reveal_type(x8) # revealed: int | str
x9: int | str = f(lst(c))
reveal_type(x9) # revealed: int | str
# TODO: Ideally this would reveal `int | str`. This is a known limitation of our
# call inference solver, and would require an extra inference attempt without type
# context, or with type context of subsets of the union, both of which are impractical
# for performance reasons.
x10: int | str | None = f(lst(c))
reveal_type(x10) # revealed: int | str | None
This applies to built-in collection constructors as well, mirroring the behavior of collection literals:
from typing import Mapping, Sequence
x1: list[int | str] | list[int | None] = list((1, 2, 3))
reveal_type(x1) # revealed: list[int | str]
x2: Sequence[int | str] | Sequence[int | None] = list((1, 2, 3))
reveal_type(x2) # revealed: list[int]
x3: list[int] | list[int | None] | list[str | None] = list(("1", "2"))
reveal_type(x3) # revealed: list[str | None]
x4: dict[str, list[int | None]] | dict[str, list[str | None]] = dict([("a", ["b"])])
reveal_type(x4) # revealed: dict[str, list[str | None]]
x5: Mapping[str, list[int | None]] | Mapping[str, list[str | None]] = dict([("a", ["b"])])
reveal_type(x5) # revealed: dict[str, list[str | None]]
def _(x6: list[dict[str, list[int] | int] | dict[str, list[int]]]):
x6.append(reveal_type(dict([("b", 1)]))) # revealed: dict[str, list[int] | int]
type EitherList = list[int | str] | list[int | None]
x7: EitherList = list((None, None))
reveal_type(x7) # revealed: list[int | None]
x8: EitherList = list(("1", "2", "3"))
reveal_type(x8) # revealed: list[int | str]
The type displayed in an invalid assignment diagnostic should account for the type context, e.g., to avoid literal promotion:
from typing import Literal, TypedDict
def f[T](x: T) -> list[T]:
return [x]
# error: [invalid-assignment] "Object of type `list[Literal["hello"] | int]` is not assignable to `list[Literal["hello"] | bool]`"
x1: list[Literal["hello"] | bool] = ["hello", 1]
class A(TypedDict):
bar: int
# error: [invalid-assignment] "Object of type `list[A | int]` is not assignable to `list[A | bool]`"
x2: list[A | bool] = [{"bar": 1}, 1]
However, the declared type should be ignored if the specialization is not solvable:
from typing import Any, Callable
def g[T](x: list[T]) -> T:
return x[0]
def _(a: int | None):
# error: [invalid-assignment] "Object of type `list[int | None]` is not assignable to `list[str]`"
x1: list[str] = f(a)
# error: [invalid-assignment] "Object of type `int | None` is not assignable to `str`"
x2: str = g(f(a))
def make_callable[T](x: T) -> Callable[[T], bool]:
raise NotImplementedError
def _(a: int | None):
# error: [invalid-assignment] "Object of type `(int | None, /) -> bool` is not assignable to `(str, /) -> bool`"
x1: Callable[[str], bool] = make_callable(a)
Both meta and class/instance attribute annotations are used as type context:
from typing import Literal, Any
class DataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> list[Literal[1]]:
return []
def __set__(self, instance: object, value: list[Literal[1]]) -> None:
pass
def _(flag: bool):
class Meta(type):
if flag:
x: DataDescriptor = DataDescriptor()
class C(metaclass=Meta):
x: list[int | None]
def _(c: C):
c.x = reveal_type([1]) # revealed: list[int]
# TODO: Use the parameter type of `__set__` as type context to avoid this error.
# error: [invalid-assignment]
C.x = [1]
For union targets, each element of the union is considered as a separate type context:
from typing import Literal
class X:
x: list[int | str]
class Y:
x: list[int | None]
def _(xy: X | Y):
xy.x = reveal_type([1]) # revealed: list[int]
The type context of all matching overloads are considered during argument inference:
from concurrent.futures import Future
from os.path import abspath
from typing import Awaitable, Callable, TypeVar, Union, 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
x1 = f1(reveal_type([1]), 1) # revealed: list[int | None]
reveal_type(x1) # revealed: int
x2 = f1(reveal_type([1]), int_or_str()) # revealed: list[int]
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: ...
x4 = f3(reveal_type({"x": [1]}), "1") # revealed: TD2
reveal_type(x4) # revealed: str
x5 = f3(reveal_type({"x": [1]}), int_or_str()) # revealed: dict[str, list[int]]
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 | None] | set[int]
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
x9 = f7(reveal_type(["Sheet1"])) # revealed: list[int | str]
reveal_type(x9) # revealed: list[int | str]
def f8(xs: tuple[str, ...]) -> tuple[str, ...]:
return tuple(map(abspath, xs))
T2 = TypeVar("T2")
def sink(func: Callable[[], Union[Awaitable[T2], T2]], future: Future[T2]) -> None:
raise NotImplementedError
# TODO: This should not error once we conjoin constraints from all call arguments.
def f9(func: Callable[[], Union[Awaitable[T2], T2]]) -> Future[T2]:
future: Future[T2] = Future()
# error: [invalid-argument-type]
sink(func, future)
return future
The parameters of both __init__ and __new__ are used as type context sources for constructor
calls:
def f[T](x: T) -> list[T]:
return [x]
class A:
def __new__(cls, value: list[int | str]):
return super().__new__(cls)
def __init__(self, value: list[int | None]): ...
A(f(1))
# error: [invalid-argument-type] "Argument to constructor `A.__new__` is incorrect: Expected `list[int | str]`, found `list[list[Unknown]]`"
A(f([]))
The type context is propagated through both branches of conditional expressions:
def f[T](x: T) -> list[T]:
raise NotImplementedError
def _(flag: bool):
x1 = f(1) if flag else f(2)
reveal_type(x1) # revealed: list[int]
x2: list[int | None] = f(1) if flag else f(2)
reveal_type(x2) # revealed: list[int | None]
When a boolean or conditional expression combines a fresh collection literal with another operand, the other operand can provide type context for the literal:
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Literal, TypedDict, reveal_type
type Key = Literal["foo", "bar"]
class Payload(TypedDict):
required: int
def from_or(values: list[str] | None) -> None:
for value in reveal_type(values or []): # revealed: list[str]
reveal_type(value) # revealed: str
def constructor_fallback(values: list[int] | None) -> None:
reveal_type(values or list()) # revealed: (list[int] & ~AlwaysFalsy) | list[Unknown]
def from_and(values: list[str]) -> None:
reveal_type(values and []) # revealed: list[str]
def chained_or(first: list[int], second: list[str]) -> None:
for value in first or second or []:
reveal_type(value) # revealed: int | str
def from_conditional(values: set[str], allowed: set[str] | None) -> None:
filtered = reveal_type(
sorted(value for value in values if value not in allowed) # revealed: list[str]
if allowed is not None
else []
)
for value in filtered:
reveal_type(value) # revealed: str
def collection_literal_first(values: list[str], flag: bool) -> None:
reveal_type([] if flag else values) # revealed: list[str]
def non_empty_dict_fallback(values: dict[Key, int] | None) -> None:
reveal_type(values or {"foo": 0}) # revealed: dict[Literal["foo", "bar"], int]
def non_empty_set_fallback(values: set[Key] | None) -> None:
reveal_type(values or {"foo"}) # revealed: set[Literal["foo", "bar"]]
class TextContent: ...
class TagContent: ...
def expects_list_content(content: list[TextContent | TagContent]) -> None: ...
def optional_content(content: list[TextContent | TagContent] | None) -> None:
expects_list_content(content or [TextContent()])
def invalid_fallback(content: list[TextContent | TagContent] | None) -> None:
expects_list_content(content or [object()]) # error: [invalid-argument-type]
def preserve_generic[T](value: T) -> T:
return value
def preserve_partially_specialized[T](value: list[T | int]) -> list[T | int]:
return value
def generic_type_context(values: list[int | str] | None) -> None:
reveal_type(preserve_generic(values or [])) # revealed: list[int | str]
reveal_type(preserve_partially_specialized(values or [])) # revealed: list[Unknown | int]
def widened_non_empty_fallback(values: list[int] | None) -> None:
result = values or ["x"]
reveal_type(result) # revealed: (list[int] & ~AlwaysFalsy) | list[int | str]
def incompatible_collection_kind(values: set[str] | None) -> None:
reveal_type(values or [1]) # revealed: (set[str] & ~AlwaysFalsy) | list[int]
def typed_dict_peer_is_only_a_hint(value: Payload | None, flag: bool) -> None:
value or {}
{} if flag else value
value or {"other": 1}
def stored_literal_is_not_fresh(values: dict[Key, int] | None) -> None:
fallback = {"foo": 0}
reveal_type(fallback) # revealed: dict[str, int]
result = values or fallback
reveal_type(result) # revealed: (dict[Key, int] & ~AlwaysFalsy) | dict[str, int]
@dataclass
class SortParams[F]:
field: F
direction: Literal["asc", "desc"] = "desc"
def build_sort_spec[T](
sort_params: SortParams[T] | None,
) -> dict[T, Literal[1, -1]] | None:
if not sort_params:
return None
return {sort_params.field: 1}
type Path = Literal["name", "age", "created"]
def use_sort(value: Mapping[Path, Literal[1, -1]]) -> None: ...
params: SortParams[Path] | None = None
sort = build_sort_spec(params) or {"name": -1}
use_sort(sort)
If a lambda expression is annotated as a Callable type, the body of the lambda is inferred with
the annotated return type as type context, and the annotated parameter types are respected:
from typing import Callable, TypedDict
class Bar(TypedDict):
bar: int
def id[T](x: T) -> T:
return x
f1 = lambda x: {"bar": 1}
reveal_type(f1) # revealed: (x) -> dict[str, int]
f2: Callable[[int], Bar] = lambda x: {"bar": 1}
reveal_type(f2) # revealed: (x: int) -> Bar
# error: [missing-typed-dict-key] "Missing required key 'bar' in TypedDict `Bar` constructor"
# error: [invalid-assignment] "Object of type `(x: int) -> dict[Unknown, Unknown]` is not assignable to `(int, /) -> Bar`"
f3: Callable[[int], Bar] = lambda x: {}
reveal_type(f3) # revealed: (int, /) -> Bar
f4: Callable[[str], str] = lambda x: reveal_type(x) # revealed: str
reveal_type(f4) # revealed: (x: str) -> str
f5: Callable[[str], str] = id(lambda x: reveal_type(x)) # revealed: str
reveal_type(f5) # revealed: (x: str) -> str
# The same return-context propagation works for generic calls whose context solves a ParamSpec.
def id_callable[**P, R](x: Callable[P, R]) -> Callable[P, R]:
return x
f5_paramspec: Callable[[int], int] = id_callable(lambda x: reveal_type(x)) # revealed: int
reveal_type(f5_paramspec) # revealed: (x: int) -> int
# TODO: This should not error once we support `Unpack`.
# error: [invalid-assignment]
f6: Callable[[*tuple[int, ...]], None] = lambda x, y, z: None
reveal_type(f6) # revealed: (*tuple[int, ...]) -> None
f7: Callable[[int, str], None] = lambda *args: None
reveal_type(f7) # revealed: (*args) -> None
# N.B. `Callable` annotations only support positional parameters.
# error: [invalid-assignment]
f8: Callable[[int], None] = lambda *, x=1: None
reveal_type(f8) # revealed: (int, /) -> None
# `Callable` annotations only describe positional parameters, so the keyword-only `x` is not
# compatible with the positional suffix in the annotation.
# error: [invalid-assignment]
f9: Callable[[*tuple[int, ...], int], None] = lambda *args, x=1: None
reveal_type(f9) # revealed: (*tuple[int, ...], int) -> None
f10: Callable[[str, int, str], tuple[str, int, str]] = lambda x, y, z: reveal_type((x, y, z)) # revealed: tuple[str, int, str]
reveal_type(f10) # revealed: (x: str, y: int, z: str) -> tuple[str, int, str]
# TODO: This should reveal `tuple[int, ...]` once we support `Unpack`.
f11: Callable[[*tuple[int, ...]], tuple[int, ...]] = lambda *args: reveal_type(args) # revealed: tuple[Unknown, ...]
reveal_type(f11) # revealed: (*args) -> tuple[Unknown, ...]
def _(x: list[int]):
f12 = list(map(lambda y: reveal_type(y) + 1, x)) # revealed: int
reveal_type(f12) # revealed: list[int]
def _() -> Callable[[int], int]:
return id(lambda x: reveal_type(x)) # revealed: int
def _():
def takes_callable(_: Callable[[int], int]): ...
takes_callable(lambda x: reveal_type(x)) # revealed: int
takes_callable(id(id(lambda x: reveal_type(x)))) # revealed: int
def _(x: bool):
signatures = {
"upper": str.upper,
"lower": str.lower,
"title": str.title,
}
# revealed: (x) -> Unknown
f = signatures.get("", reveal_type(lambda x: x))
We do not currently account for type annotations present later in the scope:
f12 = lambda: [1]
# TODO: This should not error.
_: list[int | str] = f12() # error: [invalid-assignment]
reveal_type(f12) # revealed: () -> list[int]
Generic call arguments are inferred under fixpoint iteration, allowing constraints from call arguments to contribute type context to sibling arguments within a given generic call, until convergence.
from typing import Any, Callable, Literal, Sequence, TypedDict, TypeVar, overload
def combine[T](x: T, y: list[T], z: list[T]) -> T:
return x
def combine_reversed[T](x: T, z: list[T], y: list[T]) -> T:
return x
def _(x: int, y: int | str, z: int | str | None):
x1: int | str | None = combine(y, [x], [z])
reveal_type(x1) # revealed: int | str | None
x2 = combine(y, [x], [z])
reveal_type(x2) # revealed: int | str | None
x3 = combine_reversed(y, [z], [x])
reveal_type(x3) # revealed: int | str | None
def collection_pair[T](pair: tuple[T, list[T]]) -> T:
return pair[0]
x = collection_pair((1, [True]))
reveal_type(x) # revealed: int
def callable_pair[T](pair: tuple[Callable[[T], int], list[T]]) -> None:
function, values = pair
function(values[0])
callable_pair((lambda value: reveal_type(value) + 1, [1])) # revealed: int
def nested_pair[T](pair: tuple[T, list[T]]) -> T:
return pair[0]
x = nested_pair(("value", [None]))
reveal_type(x) # revealed: str | None
class A(TypedDict):
a: int
b: int
def pair_with_list[T](x: T, y: list[T]) -> T:
return x
def pair_with_sequence[T](x: T, y: Sequence[T]) -> T:
return x
def list_pair[T](x: list[T], y: list[T]) -> T:
return x[0]
def pair[T](x: T, y: T) -> T:
return x
def _(a: A, b: list[A]):
x1: A = pair_with_list(a, [{"a": 1, "b": 2}])
reveal_type(x1) # revealed: A
# TODO: This should solve to `A`.
x2 = pair_with_list(a, [{"a": 1, "b": 2}])
reveal_type(x2) # revealed: A | dict[str, int]
x3 = pair_with_sequence(a, [{"a": 1, "b": 2}])
reveal_type(x3) # revealed: A
# TODO: This should solve to `A`.
x4 = list_pair(b, [{"a": 1, "b": 2}]) # error: [invalid-argument-type]
reveal_type(x4) # revealed: A | dict[str, int]
x5 = pair({"a": 1, "b": 2}, a)
reveal_type(x5) # revealed: A
x6 = pair(a, {"a": 1, "b": 2})
reveal_type(x6) # revealed: A
from typing import TypedDict, reveal_type
class TD(TypedDict):
x: int
def f[T](x: T, y: T) -> T:
return x
def _(td: TD):
# revealed: TD
x = reveal_type(f(td, reveal_type({"x": 1}))) # revealed: TD
# TODO: Generic call narrowing on `reveal_type` happens to choose
# the `dict` constraint here instead of `TD`, failing to narrow the
# dictionary literal.
x = f(td, reveal_type({"x": 1})) # revealed: dict[str, int]
reveal_type(x) # revealed: TD | dict[str, int]
class ActiveInitializer[T]:
def __new__(cls, *args: object) -> "ActiveInitializer[T]":
return super().__new__(cls)
def __init__(self, value: T, values: list[T]) -> None:
pass
x = ActiveInitializer(1, [True])
reveal_type(x) # revealed: ActiveInitializer[int]
class InactiveInitializer:
def __new__[T](cls, value: T, values: list[T]) -> T:
return value
def __init__(self) -> None:
pass
x = InactiveInitializer(1, [True])
reveal_type(x) # revealed: int
def consume_and_produce[T, R](
consumer: Callable[[T], R],
producer: Callable[[], T],
value: T,
) -> T:
produced = producer()
consumer(produced)
consumer(value)
return produced
x = consume_and_produce(
lambda x: reveal_type(x), # revealed: str | int
lambda: "s",
1,
)
reveal_type(x) # revealed: Literal["s", 1]
def nested_callable[T](
value: T,
callbacks: Sequence[Callable[[Callable[[T], None]], None]],
) -> None:
pass
nested_callable(
1,
[lambda callable: print(reveal_type(callable))], # revealed: (int, /) -> None
)
class Base: ...
class Dog(Base): ...
class Cat(Base): ...
BaseType = TypeVar("BaseType", bound=Base)
def register_handlers(handlers: dict[str, type[BaseType]]) -> None: ...
register_handlers({"dog": Dog, "cat": Cat})
class X: ...
def accept_classes[T: X](classes: list[type[T]]) -> None: ...
accept_classes([X])
FloatDtype = type[float] | Literal["float"]
@overload
def overloaded_call(data: Sequence[str], dtype: object) -> str: ...
@overload
def overloaded_call(data: list[Any], dtype: FloatDtype) -> float: ...
@overload
def overloaded_call[T](data: Sequence[T], dtype: Literal["generic"]) -> T: ...
def overloaded_call(data: object, dtype: object) -> object:
return data
def _(dtype: FloatDtype):
x = overloaded_call([1.0], dtype)
reveal_type(x) # revealed: int | float
from typing import Protocol, runtime_checkable
@runtime_checkable
class TakesInt(Protocol):
def __call__(
self,
tag: Literal["int"],
callback: Callable[[int], int],
) -> int: ...
@runtime_checkable
class TakesStr(Protocol):
def __call__(
self,
tag: Literal["str"],
callback: Callable[[str], str],
) -> str: ...
def _(callback: TakesInt) -> None:
if isinstance(callback, TakesStr):
reveal_type(callback) # revealed: TakesInt & TakesStr
# TODO: Perform fixpoint iteration when evaluating callable intersections.
x1 = callback("int", lambda value: reveal_type(value) + 1) # revealed: Unknown
reveal_type(x1) # revealed: int
# TODO: Perform fixpoint iteration when evaluating callable intersections.
x2 = callback("str", lambda value: reveal_type(value) + "!") # revealed: Unknown
reveal_type(x2) # revealed: str
Note that long chains of callables with constraint dependencies in reverse source-order may require multiple fixpoint iterations.
from typing import Callable
def chain[A, B, C, D](
first: Callable[[C], D],
second: Callable[[B], C],
third: Callable[[A], B],
source: list[A],
) -> D:
return first(second(third(source[0])))
x = chain(
lambda c: c + 1,
lambda b: b + 1,
lambda a: a + 1,
[1, 2, 3],
)
reveal_type(x) # revealed: int
The upper bound on iterations is calculated based on the number of independent occurences of inferable type variables, not the number of arguments.
from typing import Callable
def list_to_callable[T](values: list[T]) -> Callable[[T], T]:
raise NotImplementedError
def propagate[A, B, C, D](
first: Callable[[C], D],
second: Callable[[B], C],
third: Callable[[A], B],
source: list[A],
) -> D:
return first(second(third(source[0])))
def propagate_tuple[A, B, C, D](
arguments: tuple[
Callable[[C], D],
Callable[[B], C],
Callable[[A], B],
list[A],
],
) -> D:
raise NotImplementedError
def _(seed: int):
x = propagate(
list_to_callable([]),
list_to_callable([]),
list_to_callable([]),
[seed],
)
reveal_type(x) # revealed: int
x = propagate_tuple((
list_to_callable([]),
list_to_callable([]),
list_to_callable([]),
[seed],
))
reveal_type(x) # revealed: int
Only diagnostics from the final round of iteration are preserved:
def diagnostic_pair[T](value: T, values: list[T]) -> T:
return value
# error: [unresolved-reference]
diagnostic_pair(missing_name, [1])
diagnostic_pair(suppressed_missing, [1]) # ty: ignore[unresolved-reference]
def non_generic(value: int) -> int:
return value
# error: [unresolved-reference]
diagnostic_pair(non_generic(missing_argument), [1])
diagnostic_pair(non_generic(suppressed_argument), [1]) # ty: ignore[unresolved-reference]
The key and value parameters types are used as type context for __setitem__ dunder calls:
from typing import TypedDict
class Bar(TypedDict):
bar: list[int | str]
class Baz(TypedDict):
bar: list[int | None]
def _(x: dict[str, Bar]):
x["foo"] = reveal_type({"bar": [2]}) # revealed: Bar
class X:
def __setitem__(self, key: Bar, value: Bar): ...
def _(x: X):
# revealed: Bar
x[reveal_type({"bar": [1]})] = reveal_type({"bar": [2]}) # revealed: Bar
If the target is a union or intersection type, the key and value expressions may be inferred multiple times for each applicable type context:
from ty_extensions import Intersection
def _(x: X | dict[Baz, Baz]):
# revealed: dict[str, list[int]]
x[reveal_type({"bar": [1]})] = reveal_type({"bar": [2]}) # revealed: dict[str, list[int]]
class Y:
def __setitem__(self, key: Baz, value: Baz): ...
def _(x: Intersection[X, Y]):
# revealed: Bar
x[reveal_type({"bar": [1, "2"]})] = reveal_type({"bar": [3, "4"]}) # revealed: Bar
# revealed: Baz
x[reveal_type({"bar": [1, None]})] = reveal_type({"bar": [2, None]}) # revealed: Baz
Similarly, the declared type of a TypedDict key is used as type context:
from typing import Literal
class TD(TypedDict):
foo: list[int | None]
bar: list[int | str]
def _(x: TD, foo_or_bar: Literal["foo", "bar"]):
x["foo"] = reveal_type([1]) # revealed: list[int | None]
x["bar"] = reveal_type([2]) # revealed: list[int | str]
x[foo_or_bar] = reveal_type([3]) # revealed: list[int]
def _(x: TD | dict[str, list[int | float]]):
x["foo"] = reveal_type([4]) # revealed: list[int]
def _(x: Bar | Baz | dict[str, list[int | float]]):
x["bar"] = reveal_type([4]) # revealed: list[int]
As well as the value parameter type of augmented assignment dunder calls:
from typing import TypedDict
def _(bar: Bar):
bar |= reveal_type({"bar": [1]}) # revealed: Bar
class X:
def __ior__(self, other: Baz): ...
def _(x: X):
x |= reveal_type({"bar": [1]}) # revealed: Baz
def _(x: X | Bar):
x |= reveal_type({"bar": [1]}) # revealed: dict[str, list[int]]
class Y:
def __ior__(self, other: Bar): ...
def _(x: Intersection[X, Y]):
# TODO: Reveal `Bar` and `Baz` here.
x |= reveal_type({"bar": [1, "2"]}) # revealed: dict[str, list[int | str]]
x |= reveal_type({"bar": [1, None]}) # revealed: dict[str, list[int | None]]
await expressionsType context is also propagated through await expressions:
from typing import Literal
async def make_lst[T](x: T) -> list[T]:
return [x]
async def _():
x1 = await make_lst(1)
reveal_type(x1) # revealed: list[int]
x2: list[Literal[1]] = await make_lst(1)
reveal_type(x2) # revealed: list[Literal[1]]
x3: list[int | None] = await make_lst(1)
reveal_type(x3) # revealed: list[int | None]
Empty, unannotated container literals are inferred based on future uses that extend throughout the entire scope:
x1 = []
x1.append(1)
x1.append("2")
reveal_type(x1) # revealed: list[int | str]
x1_sorted = []
x1_sorted.append("x")
x1_sorted.sort()
reveal_type(x1_sorted) # revealed: list[str]
Bare empty list(), set(), and dict() calls also participate in full-scope inference. Calls
through aliases and shadowed names are deliberately not refined:
list_result = list()
list_result.append(1)
list_result.append("2")
reveal_type(list_result) # revealed: list[int | str]
set_result = set()
set_result.add(1)
set_result.add("2")
reveal_type(set_result) # revealed: set[int | str]
dict_result = dict()
dict_result["a"] = 1
dict_result["b"] = "2"
reveal_type(dict_result) # revealed: dict[str, int | str]
def make_list() -> list[str]:
result = list()
result.append(1)
reveal_type(result) # revealed: list[int | str]
return result # error: [invalid-return-type]
def make_set() -> set[str]:
result = set()
result.add(1)
reveal_type(result) # revealed: set[int | str]
return result # error: [invalid-return-type]
def make_dict() -> dict[str, str]:
result = dict()
result["x"] = 1
reveal_type(result) # revealed: dict[str, int | str]
return result # error: [invalid-return-type]
set_alias = set
aliased_result = set_alias()
aliased_result.add(1)
reveal_type(aliased_result) # revealed: set[Unknown]
from typing import Never
class Result:
def abort(self) -> Never:
raise RuntimeError
def shadowed_constructor() -> int:
set = Result
result = set()
reveal_type(result) # revealed: Result
result.abort()
return "unreachable"
class X:
def __init__(self):
self.x = []
self.x.append(1)
self.x.append("2")
reveal_type(self.x) # revealed: list[int | str]
reveal_type(X().x) # revealed: list[int | str]
def _(flag: bool):
if flag:
x2 = []
x2.append(1)
reveal_type(x2) # revealed: list[int]
else:
x2 = []
x2.append("2")
reveal_type(x2) # revealed: list[str]
def takes_list_int(x: list[int]): ...
x3 = []
takes_list_int(x3)
# TODO: This should reveal `list[int]`, but we do not currently record
# argument constraints for arbitrary function calls.
reveal_type(x3) # revealed: list[Unknown]
def append[T](x: list[T], y: T):
x.append(y)
x4 = []
append(x4, 1)
append(x4, "2")
# TODO: This should reveal `list[int | str]`, but we do not currently record
# argument constraints for arbitrary function calls.
reveal_type(x4) # revealed: list[Unknown]
x5 = []
_: list[int] = reveal_type(x5) # revealed: list[int]
def _() -> list[int | None]:
x6 = []
return reveal_type(x6) # revealed: list[int | None]
def _() -> int:
invalid_x6 = []
return invalid_x6 # error: [invalid-return-type]
x7 = []
x7[:] = [1, "2", 3.0]
reveal_type(x7) # revealed: list[int | str | float]
from typing import Literal
x8 = []
one: Literal[1] = 1
x8.append(one)
reveal_type(x8) # revealed: list[Literal[1]]
x9 = []
x10 = []
x9.append(1)
x9.append("2")
x10.append(3)
reveal_type(x9) # revealed: list[int | str]
reveal_type(x10) # revealed: list[int]
x11 = []
x12 = []
x11.append(1)
x12.append(x11)
reveal_type(x11) # revealed: list[int]
reveal_type(x12) # revealed: list[list[int]]
x13 = []
x13.append(x13)
reveal_type(x13) # revealed: list[Divergent]
x14 = []
x15 = []
x14.append(x15)
x15.append(x14)
reveal_type(x14) # revealed: list[Divergent]
reveal_type(x15) # revealed: list[Divergent]
Collection-use constraints must converge when multiple collection literals are used in a container literal. This is a regression test for https://github.com/astral-sh/ty/issues/3778:
from typing import Any
def run(cond: bool, d: dict[Any, Any]) -> list[Any]:
a = {}
b = {}
if cond:
b = d
return [a.get("x", 0), b.get("x", 0)]
def assigned(cond: bool, d: dict[Any, Any]) -> list[Any]:
a = {}
b = {}
if cond:
b = d
result: list[Any] = [a.get("x", 0), b.get("x", 0)]
return result
def _(i):
x16 = []
x16.append(x16)
reveal_type(x16) # revealed: list[Divergent]
x17 = {}
x17.update(a=1)
reveal_type(x17) # revealed: dict[str, int]
x18 = {}
x18.update({"a": 1})
reveal_type(x18) # revealed: dict[str, int]
x19 = {}
x19["a"] = 1
x19["b"] = "2"
reveal_type(x19) # revealed: dict[str, int | str]
x20 = {}
x20["a"] = len(x20)
x20.setdefault("b", str(len(x20)))
reveal_type(x20) # revealed: dict[str, int | str]
x21 = []
_: list[int] = x21 # error: [invalid-assignment]
# TODO: We should error on this `append` instead of the assignment and not union
# later constraints after the element type has been fully constrained above, to
# avoid confusing error messages where the type of the collection may be unexpectedly
# influenced by uses later in the scope.
x21.append("a")
# TODO: This would then reveal `list[int]`.
reveal_type(x21) # revealed: list[int | str]
def _(flag: bool):
if flag:
x22 = []
else:
x22 = []
x22.append(1)
# TODO: This should reveal `list[int]`.
reveal_type(x22) # revealed: list[Unknown]
x23 = [None, None, None]
x23[0] = 1
x23[1] = "2"
x23[2] = 3.0
reveal_type(x23) # revealed: list[int | str | float | None]
x24 = {"a": 1}
x24[1] = "b"
reveal_type(x24) # revealed: dict[int | str, str | int]
Diagnostics unrelated to the type-context are only reported once:
from typing import TypedDict
def lst[T](x: T) -> list[T]:
return [x]
def takes_list_of_bool(x: list[bool], y: list[bool]): ...
def takes_list_of_int(x: list[int], y: list[int]): ...
def takes_list_of_int2(x: list[int], y: list[int]): ...
def _(x: int):
if x == 0:
y = takes_list_of_bool
elif x == 1:
y = takes_list_of_int
else:
y = takes_list_of_int2
if x == 0:
z = True
y(lst(True), [True])
# error: [possibly-unresolved-reference] "Name `z` used when possibly not defined"
y(lst(True), [z])
def g[T](x: T, y: list[T | None]) -> T:
return x
def _(flag: bool):
if flag:
x = 1
# error: [possibly-unresolved-reference]
x1: int | str = g(x, [1])
reveal_type(x1) # revealed: int
if flag:
y = "1"
# error: [possibly-unresolved-reference]
x2: list[int | None] | list[str | None] = [y]
reveal_type(x2) # revealed: list[str | None]
class Bar(TypedDict):
bar: int
class Bar2(TypedDict):
bar: int
class Bar3(TypedDict):
bar: int
def _(flag: bool, bar: Bar | Bar2 | Bar3):
if flag:
y = 1
# error: [possibly-unresolved-reference]
bar |= {"bar": y}
def _(flag: bool, x: dict[Bar, Bar] | dict[Bar2, Bar2] | dict[Bar3, Bar3]):
if flag:
y = 1
# error: [possibly-unresolved-reference]
x[{"bar": y}] = {"bar": 1}
# error: [possibly-unresolved-reference]
x[{"bar": 1}] = {"bar": y}
class TD(TypedDict):
foo: Bar
def _(flag: bool, x: TD | dict[str, Bar2] | dict[str, Bar3]):
if flag:
y = 1
# error: [possibly-unresolved-reference]
x["foo"] = {"bar": y}
def takes_str(_: str): ...
def takes_str2(_: str): ...
def _(a: object, b: object, flag: bool):
if flag:
x = takes_str
else:
x = takes_str2
# error: [unsupported-operator] "Operator `>` is not supported between two objects of type `object`"
x(f"{'a' if a > b else 'b'}")
from typing import TypedDict
class HasTD:
td: Bar
def _(has_td: HasTD, flag: bool):
if flag:
y = 1
# error: [possibly-unresolved-reference] "Name `y` used when possibly not defined"
has_td.td = {"bar": y}