Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

type hint of function exceptions #14900

Closed
dfroger opened this issue Mar 14, 2023 · 2 comments
Closed

type hint of function exceptions #14900

dfroger opened this issue Mar 14, 2023 · 2 comments
Labels

Comments

@dfroger
Copy link
Contributor

dfroger commented Mar 14, 2023

Feature

More support on exception static checking.

Pitch

Statically declare the exceptions E that a function f can raise, which will be statically checked by comparing E with the exceptions F that can be raised by the functions called inside f minus the exceptions C caught by f: E = F - C.

Also discussed in typing-sig mailing list.

Example

Working with Python 3.11

from typing import Callable, NoReturn, ParamSpec, TypeVar, Protocol, cast

# ==============================================================================
# tooling
# ==============================================================================


def assert_never(value: NoReturn) -> NoReturn:
    # This also works at runtime as well
    assert False, f"This code should never be reached, got: {value}"


P = ParamSpec("P")
R = TypeVar("R", covariant=True)
E = TypeVar("E", bound=tuple)


class RaisingFunc(Protocol[P, R, E]):
    errors: E

    def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
        ...


def raises(errors: E) -> Callable[[Callable[P, R]], RaisingFunc[P, R, E]]:
    def decorator(func: Callable[P, R]) -> RaisingFunc[P, R, E]:
        func = cast(RaisingFunc[P, R, E], func)
        func.errors = errors
        return func

    return decorator


# ==============================================================================
# demo code
# ==============================================================================


class SomeError(Exception):
    ...


class AnotherError(Exception):
    ...


@raises((ValueError, SomeError, AnotherError))
def foo(i: int):
    if i == 0:
        raise ValueError("Must not be zero")
    elif i == 1:
        raise SomeError("Must not be one")
    elif i == 2:
        raise AnotherError("Must not be two")
    print("that's ok")


@raises((ValueError, AnotherError))
def bar(i: int):
    try:
        foo(i)
    except foo.errors as e:
        if isinstance(e, SomeError):
            print("ignore some error")
        else:
            raise


try:
    bar(0)
except bar.errors as e:
    if isinstance(e, ValueError):
        print("value error")
    elif isinstance(e, AnotherError):
        print("some error")
    else:
        # Here mypy checks for exhaustiveness
        assert_never(e)
        raise

Requires Mypy plugin, or new feature

class SomeError(Exception):
    ...


class AnotherError(Exception):
    ...


@raises((ValueError, SomeError, AnotherError))
def foo(i: int):
    if i == 0:
        raise ValueError("Must not be zero")
    elif i == 1:
        raise SomeError("Must not be one")
    elif i == 2:
        raise AnotherError("Must not be two")
    print("that's ok")

    # mypy checks that @raises arguments are correct, as mypy deduces
    # from the source code that (ValueError, SomeError, AnotherError)
    # can be raised.


@raises((ValueError, AnotherError))
def bar(i: int):
    try:
        foo(i)
    except foo.errors as e:
        if isinstance(e, SomeError):
            print("ignore some error")
        else:
            raise

    # mypy checks that @raises arguments are correct, as mypy deduces
    # from the source code that (ValueError, AnotherError) can be
    # raised, because mypy see that foo(i) can raise
    # (ValueError, SomeError, AnotherError), but SomeError is catched


try:
    bar(0)
except ValueError:
    print("value error")
except AnotherError:
    print("some error")
# Here mypy checks for exhaustiveness, as it knows that bar(i) can raise
# ValueError or AnotherError

Edge cases

I didn't think to much for now to edge cases or counter examples that do not work.

@ikonst
Copy link
Contributor

ikonst commented Mar 14, 2023

You'd probably need to take it to https://github.com/python/typing/issues since you're suggesting an addition to typing.

Personally I'd like to see the discussion include some learnings from other languages. For example, Java had checked exceptions pretty much from the start. Interesting to see how that fared for them. I know other languages were not quick to adopt those. But this is all probably a topic for python/typing.

@hauntsaninja
Copy link
Collaborator

Duplicate of e.g. #7326 (and agreed that python/typing is now the better repo to discuss). In general, I think checked exceptions aren't particularly viable in Python

@hauntsaninja hauntsaninja closed this as not planned Won't fix, can't repro, duplicate, stale Mar 25, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants