crates/ty_python_semantic/resources/mdtest/annotations/string.md
def f(v: "int"):
reveal_type(v) # revealed: int
def f(v: "'int'"):
reveal_type(v) # revealed: int
def f1(v: "int | str", w: "tuple[int, str]"):
reveal_type(v) # revealed: int | str
reveal_type(w) # revealed: tuple[int, str]
def f(v: tuple[int, "str"]):
reveal_type(v) # revealed: tuple[int, str]
def f(v: "Foo"):
reveal_type(v) # revealed: Foo
def f(x: "int | 'Foo'"): ...
class Foo: ...
f("not an int or a Foo") # error: [invalid-argument-type]
f(Foo()) # fine
f(42) # fine
# error: [unresolved-reference]
def f(v: "Foo"):
reveal_type(v) # revealed: Unknown
"Partially stringified" PEP-604 unions can raise TypeError on Python <3.14; we try to detect this
common runtime error:
[environment]
python-version = "3.13"
from typing import Any, TypeVar, Callable, Protocol, TypedDict, TYPE_CHECKING
class TD(TypedDict): ...
class P(Protocol):
x: int
class Meta(type):
def __or__(cls, other: str) -> Any:
return "wow, so fancy, bet type checkers can't handle this"
class UsesMeta(metaclass=Meta): ...
T = TypeVar("T")
# fmt: off
def f(
# error: [unsupported-operator]
a: int | "Foo",
# error: [unsupported-operator]
b: int | "memoryview" | bytes,
# error: [unsupported-operator]
c: "TD" | None,
# error: [unsupported-operator]
d: "P" | None,
# fine: `TypeVar.__or__` accepts strings at runtime
e: T | "Foo",
# fine: _SpecialForm.__ror__` accepts strings at runtime
f: "Foo" | Callable[..., None],
# also fine due to the custom metaclass
g: UsesMeta | "Foo",
# error: [unsupported-operator]
h: None | None,
# error: [unresolved-reference] "SomethingUndefined"
# error: [unresolved-reference] "SomethingAlsoUndefined"
i: SomethingUndefined | SomethingAlsoUndefined,
# error: [unsupported-operator]
# error: [unsupported-operator]
j: list["int" | None] | "bytes",
):
reveal_type(a) # revealed: int | Foo
reveal_type(b) # revealed: int | memoryview[int] | bytes
reveal_type(c) # revealed: TD | None
reveal_type(d) # revealed: P | None
reveal_type(e) # revealed: T@f | Foo
reveal_type(f) # revealed: Foo | ((...) -> None)
reveal_type(g) # revealed: UsesMeta | Foo
reveal_type(h) # revealed: None
reveal_type(i) # revealed: Unknown
# fmt: on
class Foo: ...
# error: [unsupported-operator]
X = list["int" | None]
if TYPE_CHECKING:
bar: "int" | "None"
def foo(x: "int" | "None"): ...
class Bar:
# no error because this annotation is resolved inside a scope
# fully defined inside an `if TYPE_CHECKING` block
def f(x: "int" | "None"): ...
This error is never emitted on stub files, because they are never executed at runtime:
[environment]
python-version = "3.13"
# fine
def f(x: "int" | None): ...
__future__ annotationsThe errors can be avoided in type-annotation contexts by using __future__ annotations on Python
<3.14:
[environment]
python-version = "3.13"
from __future__ import annotations
def f(v: int | "Foo"): # fine
reveal_type(v) # revealed: int | Foo
class Foo:
def __init__(self):
self.x: "int" | "str" = 42
d = {}
d[0]: "int" | "str" = 42
# error: [unsupported-operator]
X = list["int" | None]
Runtime errors are also less common for partially stringified annotations if the Python version being used is >=3.14:
[environment]
python-version = "3.14"
def f(v: int | "Foo"): # fine
reveal_type(v) # revealed: int | Foo
class Foo: ...
# error: [unsupported-operator]
X = list["int" | None]
typing.Literalfrom typing import Literal
def f1(v: Literal["Foo", "Bar"], w: 'Literal["Foo", "Bar"]'):
reveal_type(v) # revealed: Literal["Foo", "Bar"]
reveal_type(w) # revealed: Literal["Foo", "Bar"]
class Foo: ...
def f1(
# error: [raw-string-type-annotation] "Raw string literals are not allowed in parameter annotations"
a: r"int",
# error: [raw-string-type-annotation] "Raw string literals are not allowed in parameter annotations"
b: list[r"int"],
# error: [invalid-type-form] "F-strings are not allowed in parameter annotations"
c: f"int",
# error: [invalid-type-form] "F-strings are not allowed in parameter annotations"
d: list[f"int"],
# error: [invalid-type-form] "Bytes literals are not allowed in this context in a parameter annotation"
e: b"int",
f: "int",
# error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
g: "in" "t",
# error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
h: list["in" "t"],
# error: [escape-character-in-forward-annotation] "Escape characters are not allowed in parameter annotations"
i: "\N{LATIN SMALL LETTER I}nt",
# error: [escape-character-in-forward-annotation] "Escape characters are not allowed in parameter annotations"
j: "\x69nt",
k: """int""",
# error: [invalid-type-form] "Bytes literals are not allowed in this context in a parameter annotation"
l: "b'int'",
# error: [invalid-type-form] "Bytes literals are not allowed in this context in a parameter annotation"
m: list[b"int"],
): # fmt:skip
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: list[Unknown]
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: list[Unknown]
reveal_type(e) # revealed: Unknown
reveal_type(f) # revealed: int
reveal_type(g) # revealed: Unknown
reveal_type(h) # revealed: list[Unknown]
reveal_type(i) # revealed: Unknown
reveal_type(j) # revealed: Unknown
reveal_type(k) # revealed: int
reveal_type(l) # revealed: Unknown
reveal_type(m) # revealed: list[Unknown]
typing.Literalfrom typing import Literal
def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]): # fmt:skip
reveal_type(v) # revealed: Literal["a", "b", "de", "f", "g", "h", b"c"]
MyType = int
class Aliases:
MyType = str
forward: "MyType" = "value"
not_forward: MyType = "value"
reveal_type(Aliases.forward) # revealed: str
reveal_type(Aliases.not_forward) # revealed: str
a: "int" = 1
b: "'int'" = 1
# error: [invalid-syntax-in-forward-annotation] "too many levels of nested string annotations; remove the redundant nested quotes"
c: """'"int"'""" = 1
d: "Foo"
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Foo`"
e: "Foo" = 1
# error: [invalid-syntax-in-forward-annotation] "nested string annotation is too long; remove the redundant nested quotes"
f: "'str | int | bool | Foo | Bar'" = 1
class Foo: ...
d = Foo()
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Literal[1]
reveal_type(c) # revealed: Literal[1]
reveal_type(d) # revealed: Foo
reveal_type(e) # revealed: Foo
reveal_type(f) # revealed: Literal[1]
TODO: Add tests once parameter inference is supported
The expressions in these string annotations aren't valid expressions in this context but we shouldn't panic.
# Regression test for https://github.com/astral-sh/ty/issues/1865
# error: [invalid-type-form]
stringified_fstring_with_conditional: "f'{1 if 1 else 1}'"
# error: [invalid-type-form]
stringified_fstring_with_boolean_expression: "f'{1 or 2}'"
# error: [invalid-type-form]
stringified_fstring_with_generator_expression: "f'{(i for i in range(5))}'"
# error: [invalid-type-form]
stringified_fstring_with_list_comprehension: "f'{[i for i in range(5)]}'"
# error: [invalid-type-form]
stringified_fstring_with_dict_comprehension: "f'{ {i: i for i in range(5)} }'"
# error: [invalid-type-form]
stringified_fstring_with_set_comprehension: "f'{ {i for i in range(5)} }'"
# error: [invalid-type-form]
a: "1 or 2"
# error: [invalid-type-form]
b: "(x := 1)"
# error: [invalid-type-form]
c: "1 + 2"
# Regression test for https://github.com/astral-sh/ty/issues/1847
# error: [invalid-type-form]
c2: "a*(i for i in [])"
# error: [invalid-type-form]
d: "lambda x: x"
# error: [invalid-type-form]
e: "x if True else y"
# error: [invalid-type-form]
f: "{'a': 1, 'b': 2}"
# error: [invalid-type-form]
g: "{1, 2}"
# error: [invalid-type-form]
h: "[i for i in range(5)]"
# error: [invalid-type-form]
i: "{i for i in range(5)}"
# error: [invalid-type-form]
j: "{i: i for i in range(5)}"
# error: [invalid-type-form]
k: "(i for i in range(5))"
# error: [invalid-type-form]
l: "await 1"
# error: [invalid-syntax-in-forward-annotation]
m: "yield 1"
# error: [invalid-syntax-in-forward-annotation]
n: "yield from 1"
# error: [invalid-type-form]
o: "1 < 2"
# error: [invalid-type-form]
p: "call()"
# error: [invalid-type-form] "List literals are not allowed"
r: "[1, 2]"
# error: [invalid-type-form] "Tuple literals are not allowed"
s: "(1, 2)"
Quoted type annotations should be parsed as if surrounded by parentheses.
def valid(
a1: """(
int |
str
)
""",
a2: """
int |
str
""",
):
reveal_type(a1) # revealed: int | str
reveal_type(a2) # revealed: int | str
def invalid(
# error: [invalid-syntax-in-forward-annotation]
a1: """
int |
str)
""",
# error: [invalid-syntax-in-forward-annotation]
a2: """
int) |
str
""",
# error: [invalid-syntax-in-forward-annotation]
a3: """
(int)) """,
):
pass