mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-18 19:41:34 +00:00
## Summary Resolves https://github.com/astral-sh/ty/issues/1228. This PR is stacked on https://github.com/astral-sh/ruff/pull/21210.
19 KiB
19 KiB
Assignment with annotations
Annotation only transparent to local inference
x = 1
x: int
y = x
reveal_type(y) # revealed: Literal[1]
Violates own annotation
x: int = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
Violates previous annotation
x: int
x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
Tuple annotations are understood
[environment]
python-version = "3.12"
module.py:
from typing_extensions import Unpack
a: tuple[()] = ()
b: tuple[int] = (42,)
c: tuple[str, int] = ("42", 42)
d: tuple[tuple[str, str], tuple[int, int]] = (("foo", "foo"), (42, 42))
e: tuple[str, ...] = ()
f: tuple[str, *tuple[int, ...], bytes] = ("42", b"42")
g: tuple[str, Unpack[tuple[int, ...]], bytes] = ("42", b"42")
h: tuple[list[int], list[int]] = ([], [])
i: tuple[str | int, str | int] = (42, 42)
j: tuple[str | int] = (42,)
script.py:
from module import a, b, c, d, e, f, g, h, i, j
reveal_type(a) # revealed: tuple[()]
reveal_type(b) # revealed: tuple[int]
reveal_type(c) # revealed: tuple[str, int]
reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
reveal_type(e) # revealed: tuple[str, ...]
reveal_type(f) # revealed: tuple[str, *tuple[int, ...], bytes]
reveal_type(g) # revealed: tuple[@Todo(PEP 646), ...]
reveal_type(h) # revealed: tuple[list[int], list[int]]
reveal_type(i) # revealed: tuple[str | int, str | int]
reveal_type(j) # revealed: tuple[str | int]
Incorrect tuple assignments are complained about
# error: [invalid-assignment] "Object of type `tuple[Literal[1], Literal[2]]` is not assignable to `tuple[()]`"
a: tuple[()] = (1, 2)
# error: [invalid-assignment] "Object of type `tuple[Literal["foo"]]` is not assignable to `tuple[int]`"
b: tuple[int] = ("foo",)
# error: [invalid-assignment]
c: tuple[str | int, str] = ([], "foo")
Collection literal annotations are understood
[environment]
python-version = "3.12"
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]
Optional collection literal annotations are understood
[environment]
python-version = "3.12"
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]
Incorrect collection literal assignments are complained about
# error: [invalid-assignment] "Object of type `list[Unknown | int]` is not assignable to `list[str]`"
a: list[str] = [1, 2, 3]
# error: [invalid-assignment] "Object of type `set[Unknown | int | str]` is not assignable to `set[int]`"
b: set[int] = {1, 2, "3"}
Literal annnotations are respected
[environment]
python-version = "3.12"
from enum import Enum
from typing_extensions import Literal, LiteralString
a: list[Literal[1]] = [1]
reveal_type(a) # revealed: list[Literal[1]]
b: list[Literal[True]] = [True]
reveal_type(b) # revealed: list[Literal[True]]
c: list[Literal["a"]] = ["a"]
reveal_type(c) # revealed: list[Literal["a"]]
d: list[LiteralString] = ["a", "b", "c"]
reveal_type(d) # revealed: list[LiteralString]
e: list[list[Literal[1]]] = [[1]]
reveal_type(e) # revealed: list[list[Literal[1]]]
class Color(Enum):
RED = "red"
f: dict[list[Literal[1]], list[Literal[Color.RED]]] = {[1]: [Color.RED, Color.RED]}
reveal_type(f) # revealed: dict[list[Literal[1]], list[Color]]
class X[T]:
def __init__(self, value: T): ...
g: X[Literal[1]] = X(1)
reveal_type(g) # revealed: X[Literal[1]]
h: X[int] = X(1)
reveal_type(h) # revealed: X[int]
i: dict[list[X[Literal[1]]], set[Literal[b"a"]]] = {[X(1)]: {b"a"}}
reveal_type(i) # revealed: dict[list[X[Literal[1]]], set[Literal[b"a"]]]
j: list[Literal[1, 2, 3]] = [1, 2, 3]
reveal_type(j) # revealed: list[Literal[1, 2, 3]]
k: list[Literal[1] | Literal[2] | Literal[3]] = [1, 2, 3]
reveal_type(k) # revealed: list[Literal[1, 2, 3]]
type Y[T] = list[T]
l: Y[Y[Literal[1]]] = [[1]]
reveal_type(l) # revealed: list[Y[Literal[1]]]
m: list[tuple[Literal[1], Literal[2], Literal[3]]] = [(1, 2, 3)]
reveal_type(m) # revealed: list[tuple[Literal[1], Literal[2], Literal[3]]]
n: list[tuple[int, str, int]] = [(1, "2", 3), (4, "5", 6)]
reveal_type(n) # revealed: list[tuple[int, str, int]]
o: list[tuple[Literal[1], ...]] = [(1, 1, 1)]
reveal_type(o) # revealed: list[tuple[Literal[1], ...]]
p: list[tuple[int, ...]] = [(1, 1, 1)]
reveal_type(p) # revealed: list[tuple[int, ...]]
# literal promotion occurs based on assignability, an exact match is not required
q: list[int | Literal[1]] = [1]
reveal_type(q) # revealed: list[int]
r: list[Literal[1, 2, 3, 4]] = [1, 2]
reveal_type(r) # revealed: list[Literal[1, 2, 3, 4]]
s: list[Literal[1]]
s = [1]
reveal_type(s) # revealed: list[Literal[1]]
(s := [1])
reveal_type(s) # revealed: list[Literal[1]]
Generic constructor annotations are understood
[environment]
python-version = "3.12"
from typing import Any
class X[T]:
def __init__(self, value: T):
self.value = value
a: X[int] = X(1)
reveal_type(a) # revealed: X[int]
b: X[int | None] = X(1)
reveal_type(b) # revealed: X[int | None]
c: X[int | None] | None = X(1)
reveal_type(c) # revealed: X[int | None]
def _[T](a: X[T]):
b: X[T | int] = X(a.value)
reveal_type(b) # revealed: X[T@_ | int]
d: X[Any] = X(1)
reveal_type(d) # revealed: X[Any]
def _(flag: bool):
# TODO: Handle unions correctly.
# error: [invalid-assignment] "Object of type `X[int]` is not assignable to `X[int | None]`"
a: X[int | None] = X(1) if flag else X(2)
reveal_type(a) # revealed: X[int | None]
from dataclasses import dataclass
@dataclass
class Y[T]:
value: T
y1: Y[Any] = Y(value=1)
# TODO: This should reveal `Y[Any]`.
reveal_type(y1) # revealed: Y[int]
class Z[T]:
def __new__(cls, value: T):
return super().__new__(cls)
z1: Z[Any] = Z(1)
# TODO: This should reveal `Z[Any]`.
reveal_type(z1) # revealed: Z[int]
PEP-604 annotations are supported
def foo(v: str | int | None, w: str | str | None, x: str | str):
reveal_type(v) # revealed: str | int | None
reveal_type(w) # revealed: str | None
reveal_type(x) # revealed: str
PEP-604 in non-type-expression context
In Python 3.10 and later
[environment]
python-version = "3.10"
IntOrStr = int | str
Earlier versions
[environment]
python-version = "3.9"
# error: [unsupported-operator]
IntOrStr = int | str
Attribute expressions in type annotations are understood
import builtins
int = "foo"
a: builtins.int = 42
# error: [invalid-assignment] "Object of type `Literal["bar"]` is not assignable to `int`"
b: builtins.int = "bar"
c: builtins.tuple[builtins.tuple[builtins.int, builtins.int], builtins.int] = ((42, 42), 42)
# error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `tuple[tuple[int, int], int]`"
c: builtins.tuple[builtins.tuple[builtins.int, builtins.int], builtins.int] = "foo"
Future annotations are deferred
from __future__ import annotations
x: Foo
class Foo: ...
x = Foo()
reveal_type(x) # revealed: Foo
Annotations in stub files are deferred
x: Foo
class Foo: ...
x = Foo()
reveal_type(x) # revealed: Foo
Annotations are deferred by default in Python 3.14 and later
[environment]
python-version = "3.14"
x: Foo
class Foo: ...
x = Foo()
reveal_type(x) # revealed: Foo
Annotated assignments in stub files are inferred correctly
x: int = 1
reveal_type(x) # revealed: Literal[1]
Annotations influence generic call inference
[environment]
python-version = "3.12"
generic_list.py:
from typing import Literal
def f[T](x: T) -> list[T]:
return [x]
a = f("a")
reveal_type(a) # revealed: list[Literal["a"]]
b: list[int | Literal["a"]] = f("a")
reveal_type(b) # revealed: list[int | Literal["a"]]
c: list[int | str] = f("a")
reveal_type(c) # revealed: list[int | str]
d: list[int | tuple[int, int]] = f((1, 2))
reveal_type(d) # revealed: list[int | tuple[int, int]]
e: list[int] = f(True)
reveal_type(e) # revealed: list[int]
# error: [invalid-assignment] "Object of type `list[Literal["a"]]` is not assignable to `list[int]`"
g: list[int] = f("a")
# error: [invalid-assignment] "Object of type `list[Literal["a"]]` is not assignable to `tuple[int]`"
h: tuple[int] = f("a")
def f2[T: int](x: T) -> T:
return x
i: int = f2(True)
reveal_type(i) # revealed: Literal[True]
j: int | str = f2(True)
reveal_type(j) # revealed: Literal[True]
A function's arguments are also inferred using the type context:
typed_dict.py:
from typing import TypedDict
class TD(TypedDict):
x: int
def f[T](x: list[T]) -> T:
return x[0]
a: TD = f([{"x": 0}, {"x": 1}])
reveal_type(a) # revealed: TD
b: TD | None = f([{"x": 0}, {"x": 1}])
reveal_type(b) # revealed: TD
# error: [missing-typed-dict-key] "Missing required key 'x' in TypedDict `TD` constructor"
# error: [invalid-key] "Invalid key for TypedDict `TD`: Unknown key "y""
# error: [invalid-assignment] "Object of type `Unknown | dict[Unknown | str, Unknown | int]` is not assignable to `TD`"
c: TD = f([{"y": 0}, {"x": 1}])
# error: [missing-typed-dict-key] "Missing required key 'x' in TypedDict `TD` constructor"
# error: [invalid-key] "Invalid key for TypedDict `TD`: Unknown key "y""
# error: [invalid-assignment] "Object of type `Unknown | dict[Unknown | str, Unknown | int]` is not assignable to `TD | None`"
c: TD | None = f([{"y": 0}, {"x": 1}])
But not in a way that leads to assignability errors:
dict_any.py:
from typing import TypedDict, Any
class TD(TypedDict, total=False):
x: str
class TD2(TypedDict):
x: str
def f(self, dt: dict[str, Any], key: str):
# TODO: This should not error once typed dict assignability is implemented.
# error: [invalid-assignment]
x1: TD = dt.get(key, {})
reveal_type(x1) # revealed: TD
x2: TD = dt.get(key, {"x": 0})
reveal_type(x2) # revealed: Any
x3: TD | None = dt.get(key, {})
# TODO: This should reveal `Any` once typed dict assignability is implemented.
reveal_type(x3) # revealed: Any | None
x4: TD | None = dt.get(key, {"x": 0})
reveal_type(x4) # revealed: Any
x5: TD2 = dt.get(key, {})
reveal_type(x5) # revealed: Any
x6: TD2 = dt.get(key, {"x": 0})
reveal_type(x6) # revealed: Any
x7: TD2 | None = dt.get(key, {})
reveal_type(x7) # revealed: Any
x8: TD2 | None = dt.get(key, {"x": 0})
reveal_type(x8) # revealed: Any
Prefer the declared type of generic classes
[environment]
python-version = "3.12"
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]
a = f(1)
reveal_type(a) # revealed: list[Literal[1]]
b: list[Any] = f(1)
reveal_type(b) # revealed: list[Any]
c: list[Any] = [1]
reveal_type(c) # revealed: list[Any]
d: list[Any] | None = f(1)
reveal_type(d) # revealed: list[Any]
e: list[Any] | None = [1]
reveal_type(e) # revealed: list[Any]
f: list[Any] | None = f2(1)
# TODO: Better constraint solver.
reveal_type(f) # revealed: list[Literal[1]] | None
g: list[Any] | dict[Any, Any] = f3(1)
# TODO: Better constraint solver.
reveal_type(g) # revealed: list[Literal[1]] | dict[Literal[1], Literal[1]]
We currently prefer the generic declared type regardless of its variance:
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[Literal[1]]
reveal_type(x4) # revealed: Invariant[Literal[1]]
x5: Bivariant[Any] = bivariant(1)
x6: Covariant[Any] = covariant(1)
x7: Contravariant[Any] = contravariant(1)
x8: Invariant[Any] = invariant(1)
# TODO: This could reveal `Bivariant[Any]`.
reveal_type(x5) # revealed: Bivariant[Literal[1]]
reveal_type(x6) # revealed: Covariant[Any]
reveal_type(x7) # revealed: Contravariant[Any]
reveal_type(x8) # revealed: Invariant[Any]
Narrow generic unions
[environment]
python-version = "3.12"
from typing import reveal_type, TypedDict
def identity[T](x: T) -> T:
return x
def _(narrow: dict[str, str], target: list[str] | dict[str, str] | None):
target = identity(narrow)
reveal_type(target) # revealed: dict[str, str]
def _(narrow: list[str], target: list[str] | dict[str, str] | None):
target = identity(narrow)
reveal_type(target) # revealed: list[str]
def _(narrow: list[str] | dict[str, str], target: list[str] | dict[str, str] | None):
target = identity(narrow)
reveal_type(target) # revealed: list[str] | dict[str, str]
class TD(TypedDict):
x: int
def _(target: list[TD] | dict[str, TD] | None):
target = identity([{"x": 1}])
reveal_type(target) # revealed: list[TD]
def _(target: list[TD] | dict[str, TD] | None):
target = identity({"x": {"x": 1}})
reveal_type(target) # revealed: dict[str, TD]
Prefer the inferred type of non-generic classes
[environment]
python-version = "3.12"
def identity[T](x: T) -> T:
return x
def lst[T](x: T) -> list[T]:
return [x]
def _(i: int):
a: int | None = i
b: int | None = identity(i)
c: int | str | None = identity(i)
reveal_type(a) # revealed: int
reveal_type(b) # revealed: int
reveal_type(c) # revealed: int
a: list[int | None] | None = [i]
b: list[int | None] | None = identity([i])
c: list[int | None] | int | None = identity([i])
reveal_type(a) # revealed: list[int | None]
reveal_type(b) # revealed: list[int | None]
reveal_type(c) # revealed: list[int | None]
a: list[int | None] | None = [i]
b: list[int | None] | None = lst(i)
c: list[int | None] | int | None = lst(i)
reveal_type(a) # revealed: list[int | None]
reveal_type(b) # revealed: list[int | None]
reveal_type(c) # revealed: list[int | None]
a: list | None = []
b: list | None = identity([])
c: list | int | None = identity([])
reveal_type(a) # revealed: list[Unknown]
reveal_type(b) # revealed: list[Unknown]
reveal_type(c) # 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 | None = f(lst(b))
reveal_type(x6) # revealed: str
x7: int | str = f(lst(c))
reveal_type(x7) # revealed: int | str
x8: int | str = f(lst(c))
reveal_type(x8) # 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.
x9: int | str | None = f(lst(c))
reveal_type(x9) # revealed: int | str | None