ruff/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md
Abhijeet Prasad Bodas f4bd74ab6a
[ty] Correctly handle calls to functions marked as returning Never / NoReturn (#18333)
## Summary

`ty` does not understand that calls to functions which have been
annotated as having a return type of `Never` / `NoReturn` are terminal.

This PR fixes that, by adding new reachability constraints when call
expressions are seen. If the call expression evaluates to `Never`, the
code following it will be considered to be unreachable. Note that, for
adding these constraints, we only consider call expressions at the
statement level, and that too only inside function scopes. This is
because otherwise, the number of such constraints becomes too high, and
evaluating them later on during type inference results in a major
performance degradation.

Fixes https://github.com/astral-sh/ty/issues/180

## Test Plan

New mdtests.

## Ecosystem changes

This PR removes the following false-positives:
- "Function can implicitly return `None`, which is not assignable to
...".
- "Name `foo` used when possibly not defind" - because the branch in
which it is not defined has a `NoReturn` call, or when `foo` was
imported in a `try`, and the except had a `NoReturn` call.

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-07-04 11:52:52 -07:00

3.3 KiB

assert_never

Basic functionality

assert_never makes sure that the type of the argument is Never.

Correct usage

from typing_extensions import assert_never, Never, Any
from ty_extensions import Unknown

def _(never: Never):
    assert_never(never)  # fine

Diagnostics

If it is not, a type-assertion-failure diagnostic is emitted.

from typing_extensions import assert_never, Never, Any
from ty_extensions import Unknown

def _():
    assert_never(0)  # error: [type-assertion-failure]

def _():
    assert_never("")  # error: [type-assertion-failure]

def _():
    assert_never(None)  # error: [type-assertion-failure]

def _():
    assert_never([])  # error: [type-assertion-failure]

def _():
    assert_never({})  # error: [type-assertion-failure]

def _():
    assert_never(())  # error: [type-assertion-failure]

def _(flag: bool, never: Never):
    assert_never(1 if flag else never)  # error: [type-assertion-failure]

def _(any_: Any):
    assert_never(any_)  # error: [type-assertion-failure]

def _(unknown: Unknown):
    assert_never(unknown)  # error: [type-assertion-failure]

Use case: Type narrowing and exhaustiveness checking

[environment]
python-version = "3.10"

assert_never can be used in combination with type narrowing as a way to make sure that all cases are handled in a series of isinstance checks or other narrowing patterns that are supported.

from typing_extensions import assert_never, Literal

class A: ...
class B: ...
class C: ...

def if_else_isinstance_success(obj: A | B):
    if isinstance(obj, A):
        pass
    elif isinstance(obj, B):
        pass
    elif isinstance(obj, C):
        pass
    else:
        assert_never(obj)

def if_else_isinstance_error(obj: A | B):
    if isinstance(obj, A):
        pass
    # B is missing
    elif isinstance(obj, C):
        pass
    else:
        # error: [type-assertion-failure] "Argument does not have asserted type `Never`"
        assert_never(obj)

def if_else_singletons_success(obj: Literal[1, "a"] | None):
    if obj == 1:
        pass
    elif obj == "a":
        pass
    elif obj is None:
        pass
    else:
        assert_never(obj)

def if_else_singletons_error(obj: Literal[1, "a"] | None):
    if obj == 1:
        pass
    elif obj is "A":  # "A" instead of "a"
        pass
    elif obj is None:
        pass
    else:
        # error: [type-assertion-failure] "Argument does not have asserted type `Never`"
        assert_never(obj)

def match_singletons_success(obj: Literal[1, "a"] | None):
    match obj:
        case 1:
            pass
        case "a":
            pass
        case None:
            pass
        case _ as obj:
            # TODO: Ideally, we would not emit an error here
            # error: [type-assertion-failure] "Argument does not have asserted type `Never`"
            assert_never(obj)

def match_singletons_error(obj: Literal[1, "a"] | None):
    match obj:
        case 1:
            pass
        case "A":  # "A" instead of "a"
            pass
        case None:
            pass
        case _ as obj:
            # TODO: We should emit an error here, but the message should
            # show the type `Literal["a"]` instead of `@Todo(…)`.
            # error: [type-assertion-failure] "Argument does not have asserted type `Never`"
            assert_never(obj)