Files
ruff/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md
Alex Waygood ca0cce3f9c [red-knot] Fix more [redundant-cast] false positives (#17170)
Fixes #17164. Simply checking whether one type is gradually equivalent
to another is too simplistic here: `Any` is gradually equivalent to
`Todo`, but we should permit users to cast from `Todo` or `Unknown` to
`Any` without complaining about it. This changes our logic so that we
only complain about redundant casts if:
- the two types are exactly equal (when normalized) OR they are
equivalent (we'll still complain about `Any -> Any` casts, and about
`Any | str | int` -> `str | int | Any` casts, since their normalized
forms are exactly equal, even though the type is not fully static -- and
therefore does not participate in equivalence relations)
- AND the casted type does not contain `Todo`
2025-04-03 15:00:00 +01:00

2.2 KiB

cast

cast() takes two arguments, one type and one value, and returns a value of the given type.

The (inferred) type of the value and the given type do not need to have any correlation.

from typing import Literal, cast, Any

reveal_type(True)  # revealed: Literal[True]
reveal_type(cast(str, True))  # revealed: str
reveal_type(cast("str", True))  # revealed: str

reveal_type(cast(int | str, 1))  # revealed: int | str

reveal_type(cast(val="foo", typ=int))  # revealed: int

# error: [invalid-type-form]
reveal_type(cast(Literal, True))  # revealed: Unknown

# error: [invalid-type-form]
reveal_type(cast(1, True))  # revealed: Unknown

# error: [missing-argument] "No argument provided for required parameter `val` of function `cast`"
cast(str)
# error: [too-many-positional-arguments] "Too many positional arguments to function `cast`: expected 2, got 3"
cast(str, b"ar", "foo")

def function_returning_int() -> int:
    return 10

# error: [redundant-cast] "Value is already of type `int`"
cast(int, function_returning_int())

def function_returning_any() -> Any:
    return "blah"

# error: [redundant-cast] "Value is already of type `Any`"
cast(Any, function_returning_any())

Complex type expressions (which may be unsupported) do not lead to spurious [redundant-cast] diagnostics.

from typing import Callable

def f(x: Callable[[dict[str, int]], None], y: tuple[dict[str, int]]):
    a = cast(Callable[[list[bytes]], None], x)
    b = cast(tuple[list[bytes]], y)

A cast from Todo or Unknown to Any is not considered a "redundant cast": even if these are understood as gradually equivalent types by red-knot, they are understood as different types by human readers of red-knot's output. For Unknown in particular, we may consider it differently in the context of some opt-in diagnostics, as it indicates that the gradual type has come about due to an invalid annotation, missing annotation or missing type argument somewhere.

from knot_extensions import Unknown

def f(x: Any, y: Unknown, z: Any | str | int):
    a = cast(dict[str, Any], x)
    reveal_type(a)  # revealed: @Todo(generics)

    b = cast(Any, y)
    reveal_type(b)  # revealed: Any

    c = cast(str | int | Any, z)  # error: [redundant-cast]