crates/ty_python_semantic/resources/mdtest/cycle.md
A lambda is always truthy. Determining whether the final assignment is reachable must not require inferring the lambda's return type, which depends on that same assignment.
(f := lambda: f)
while lambda: f:
pass
f = 0
The same cycle can arise when a conditional filters the bindings visible to a recursive lambda.
f = lambda: f
if not (lambda: f):
f = 0
Deferred annotations can result in cycles in resolving a function signature:
from __future__ import annotations
# error: [invalid-type-form]
def f(x: f):
pass
reveal_type(f) # revealed: def f(x: Unknown) -> Unknown
See: https://github.com/astral-sh/ty/issues/364
class Point:
def __init__(self, x: int = 0, y: int = 0) -> None:
self.x = x
self.y = y
def replace_with(self, other: "Point") -> None:
self.x, self.y = other.x, other.y
p = Point()
reveal_type(p.x) # revealed: int
reveal_type(p.y) # revealed: int
This is a regression test for https://github.com/astral-sh/ty/issues/3838.
while 1:
# error: [possibly-unresolved-reference]
# error: [possibly-unresolved-reference]
x = (*x, x)
while 1:
y = (y, *y)
NamedTuple with recursive fieldsThis is a regression test for https://github.com/astral-sh/ty/issues/3872. Computing the
NamedTuple fields while building the class's MRO must not try to determine whether the same class
is a TypedDict.
[environment]
python-version = "3.14"
from typing import NamedTuple
class Node[KT, VT](NamedTuple):
children: tuple[Node[KT, VT], ...] | tuple[Leaf[VT], ...]
class Leaf[VT](NamedTuple):
values: tuple[VT, ...]
This is a regression test for https://github.com/astral-sh/ty/issues/3851. Constructing a union during cycle recovery must not run redundancy checks between a literal and a protocol instance. Resolving the protocol interface can depend on the expression inference query that is already being recovered, which would introduce a new Salsa cycle.
[environment]
python-version = "3.14"
from typing import Protocol, runtime_checkable
_: Any
@property
def prop(self) -> A:
raise NotImplementedError
@runtime_checkable
class B(Protocol):
_: A
x = 5
while isinstance(x, B):
x = B() # error: [call-non-callable]
type(x)
x = 2
from typing import Any, assert_type
assert_type(prop, property)
if bool:
x = 5
while isinstance(x, B):
x = B() # error: [call-non-callable]
class A: ...
Once a recursively growing group of integer literals widens to int, later iterations must not
reintroduce individual literals. Otherwise, the inferred type continues changing and the cycle never
converges. This is a reduced regression test from SciPy's iterative sparse solvers.
def solve(maxiter, a, b, c, d, e):
iteration = 0
stop = 0
while iteration < maxiter:
iteration = iteration + 1
if iteration >= maxiter:
stop = 7
if a:
stop = 6
if b:
stop = 5
if c:
stop = 4
if d:
stop = 3
if e:
stop = 2
if stop > 0:
break
return stop
[environment]
python-version = "3.12" # typing.TypeAliasType
from typing import Union, TypeAliasType, Sequence, Mapping
A = list["A | None"]
def f(x: A):
# TODO: should be `list[A | None]`?
reveal_type(x) # revealed: list[Divergent]
# TODO: should be `A | None`?
reveal_type(x[0]) # revealed: Divergent
JSONPrimitive = Union[str, int, float, bool, None]
JSONValue = TypeAliasType("JSONValue", 'Union[JSONPrimitive, Sequence["JSONValue"], Mapping[str, "JSONValue"]]')
def _(x: JSONValue):
reveal_type(x) # revealed: Sequence[JSONValue] | float | None | Mapping[str, JSONValue]
from typing import Generic, TypeVar
B = TypeVar("B", bound="Base") # error: [missing-type-argument]
class Base(Generic[B]):
pass
This is a regression test for https://github.com/astral-sh/ty/issues/1402. When a parameter has a
default value that references the callable itself, we currently prevent infinite recursion by simply
falling back to Unknown for the type of the default value, which does not have any practical
impact except for the displayed type. We could also consider inferring Divergent when we encounter
too many layers of nesting (instead of just one), but that would require a type traversal which
could have performance implications. So for now, we mainly make sure not to panic or stack overflow
for these seemingly rare cases.
class C:
def f(self: "C"):
def inner_a(positional=self.a):
return
self.a = inner_a
# revealed: def inner_a(positional=...) -> Unknown
reveal_type(inner_a)
def inner_b(*, kw_only=self.b):
return
self.b = inner_b
# revealed: def inner_b(*, kw_only=...) -> Unknown
reveal_type(inner_b)
def inner_c(positional_only=self.c, /):
return
self.c = inner_c
# revealed: def inner_c(positional_only=..., /) -> Unknown
reveal_type(inner_c)
def inner_d(*, kw_only=self.d):
return
self.d = inner_d
# revealed: def inner_d(*, kw_only=...) -> Unknown
reveal_type(inner_d)
We do, however, still check assignability of the default value to the parameter type:
class D:
def f(self: "D"):
# error: [invalid-parameter-default] "Default value of type `(a: int = ...) -> Unknown` is not assignable to annotated parameter type `int`"
def inner_a(a: int = self.a): ...
self.a = inner_a
class C:
def f(self: "C"):
self.a = lambda positional=self.a: positional
self.b = lambda *, kw_only=self.b: kw_only
self.c = lambda positional_only=self.c, /: positional_only
self.d = lambda *, kw_only=self.d: kw_only
# revealed: (positional: Unknown = ...) -> Unknown | ((positional=...) -> Divergent)
reveal_type(self.a)
# revealed: (*, kw_only=...) -> Unknown | ((*, kw_only=...) -> Divergent)
reveal_type(self.b)
# revealed: (positional_only: Unknown = ..., /) -> Unknown | ((positional_only=..., /) -> Divergent)
reveal_type(self.c)
# revealed: (*, kw_only=...) -> Unknown | ((*, kw_only=...) -> Divergent)
reveal_type(self.d)
Resolving a decorated function's callable signature must not eagerly infer its default values. Otherwise, a default that refers back to the decorated name can re-enter the reachability check for an earlier assertion and prevent inference from converging. This is a regression test for https://github.com/astral-sh/ty/issues/4308.
f = lambda: f
assert f
@property
def f(x=lambda: f): ...
The same cycle must converge when the parameter and return type are annotated:
g = lambda: g
assert g
@property
def g(x: object = lambda: g) -> None: ...
Constructing a property explicitly has the same behavior as decorator syntax:
f = lambda: f
assert f
def getter(x=lambda: f): ...
f = property(getter)
The cycle is not specific to properties. A decorator that returns a callable with a fixed signature must also terminate:
from collections.abc import Callable
from typing import Any
def decorator(fn: Callable[[Any], Any]) -> Callable[[Any], Any]:
return fn
f = lambda: f
assert f
@decorator
def f(x=lambda: f): ...
A decorator can capture a function's parameters and return a callable with a different signature. Capturing those parameters must not evaluate a self-referential default.
[environment]
python-version = "3.12"
from collections.abc import Callable
def decorator[**P](fn: Callable[P, None]) -> Callable[[], None]:
return lambda: None
f = lambda: f
assert f
@decorator
def f(x=lambda: f) -> None: ...
reveal_type(f) # revealed: () -> None
A generic getter's annotations are inferred in its type-parameter scope. Constructing the property must not pull its self-referential default into that inference.
[environment]
python-version = "3.12"
f = lambda: f
assert f
@property
def f[T](value: T, callback=lambda: f) -> T:
return value
reveal_type(f) # revealed: property
class Cyclic:
def __init__(self, data: str | dict): # error: [missing-type-argument]
self.data = data
def update(self):
if isinstance(self.data, str):
self.data = {"url": self.data}
# revealed: str | dict[Unknown, Unknown] | dict[str, str]
reveal_type(Cyclic("").data)
Normalizing a recursive implicit-attribute type does not reinterpret specialized variadic parameters as gradual:
from typing import Any, Callable, Generic, TypeVar
from ty_extensions import static_assert
from ty_extensions._internal import TypeOf, is_subtype_of
T = TypeVar("T")
flag: bool
class C(Generic[T]):
def method(self, *args: T, **kwargs: T) -> None: ...
c = C[Any]()
class Recursive:
def __init__(self, other: "Recursive"):
self.callback = c.method if flag else other.callback
def check(value: Recursive):
reveal_type(value.callback) # revealed: bound method C[Any].method(*args: Any, **kwargs: Any) -> None
static_assert(is_subtype_of(TypeOf[value.callback], Callable[[], None]))
This is a regression test for https://github.com/astral-sh/ty/issues/3471.
from collections.abc import Callable
from typing import TypeVar
class A: ...
T = TypeVar("T")
U = TypeVar("U", bound=A)
C = Callable[[T, U], object]
def d() -> Callable[[C[U, A]], object]:
raise NotImplementedError
class B:
@d()
def m1(self, p):
pass
@d()
def m2(self, p):
self.__slots__ # error: [unresolved-attribute]
NamedTuple / NewTypeThis is a regression test for https://github.com/astral-sh/ty/issues/3485 and
https://github.com/astral-sh/ty/issues/3682. Type traversal during cycle recovery should not force
the lazy base of a NewType.
class C:
pass
def f():
pass
def g() -> T: # error: [unresolved-reference]
pass
g()
from typing import NamedTuple, NewType
X = NamedTuple("X", [("x", "X")]), None # error: [invalid-type-form]
list(X)
min(X) # error: [invalid-argument-type]
T = f()
X = NewType("X", C)
The runtime callable returned by NewType also carries the lazy base and must use the same
cycle-safe traversal.
class C: ...
def f(): ...
def g() -> T: ...
g()
from typing import NamedTuple, NewType
X = NewType("X", C)
Y = NamedTuple("Y", [("a", "Y")]), X # error: [invalid-type-form]
min(Y) # error: [invalid-argument-type]
T = f()
hasattrThis pattern used to panic with "too many cycle iterations".
class Cached:
def get(self) -> int:
return 0
@property
def metadata(self) -> int:
if not hasattr(self, "_metadata"):
self._metadata = self.get()
return self._metadata
reveal_type(Cached().metadata) # revealed: int
This example was minimized from
a real issue in robotframework.
It created
a complicated cycle with multiple cycle heads,
which also involved
a tricky Salsa behavior that comes up when a query oscillates between being a cycle head and not being one.
entry.py:
from derived import Derived
Derived.decorate
# revealed: bound method <class 'Derived'>.decorate[T](item_class: type[T]) -> type[T]
reveal_type(Derived.decorate)
derived.py:
from ty_extensions._internal import reveal_mro
import bases
class Derived(bases.GenericBase["Foo", "Bar"]): ...
@Derived.decorate
class Foo(bases.Foo): ...
# revealed: <class 'Foo'>
reveal_type(Foo)
# revealed: (<class 'derived.Foo'>, <class 'bases.Foo'>, <class 'object'>)
reveal_mro(Foo)
@Derived.decorate
class Bar(bases.Bar): ...
# revealed: <class 'Bar'>
reveal_type(Bar)
# revealed: (<class 'derived.Bar'>, <class 'bases.Bar'>, <class 'object'>)
reveal_mro(Bar)
bases.py:
from typing import Generic, TypeVar, Type
from ty_extensions._internal import reveal_mro
T = TypeVar("T")
B1 = TypeVar("B1", bound="Foo")
B2 = TypeVar("B2", bound="Bar")
class GenericBase(Generic[B1, B2]):
@classmethod
def decorate(cls, item_class: Type[T]) -> Type[T]:
return item_class
# revealed: <class 'GenericBase'>
reveal_type(GenericBase)
# revealed: (<class 'GenericBase[Unknown, Unknown]'>, typing.Generic, <class 'object'>)
reveal_mro(GenericBase)
# revealed: (<class 'GenericBase[Foo, Bar]'>, typing.Generic, <class 'object'>)
reveal_mro(GenericBase["Foo", "Bar"])
class Foo: ...
class Bar: ...