Closed as not planned
Description
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.