Comparing your NoReturn[ZeroDivisionError] syntax with the above proposed @raises(ZeroDivisionError) syntax, I’d prefer the latter. Though this is another option:
from typing import Raises
def callCat() -> Raises[Cat, CatError]: ...
The decorator syntax is a bit more pythonic, but we already have TypeGuard and TypeIs, which use the return-type-annotation instead of a decorator (the rationale for that can be found in the original PEP: PEP 647 – User-Defined Type Guards | peps.python.org ), so for consistency, I’d prefer the return-type syntax. In addition to the argument written in the PEP, I’d also add that:
Raises[Cat, CatError] can be used when using Callable: Callable[[], Raises[Cat, CatError], avoiding the need to always define a callable protocol for this
- the
Raises[Cat, CatError] syntax would also allow the use of generic exception types; this might be useful when forwarding exceptions from callbacks
Regarding the semantics, I guess the main question is whether type checkers would enforce that all uncaught exceptions by other function calls within the function body must be forwarded. I think that’s what most comments against the idea are worried about: that you end up having to declare a huge number of exceptions for any function, just because somewhere in the call stack there was a function that can raise.
First, it seems like a good idea to exclude KeyboardInterrupt and MemoryError from this mechanism because they can be raised anywhere. (Are there other exceptions like this? Maybe IOError should be excluded as well?)
Another thing that would help, I think, is if Raises[Cat] would be short-hand for Raises[Cat, Any], which conveys the information that some error may be raised, without specifying which one in particular. You can use this any time it becomes too tedious to track specific errors.
Alternatively, one could remove any kind of claim of totality from the mechanism (i.e., the declared exceptions are explicitly allowed to only represent a subset of the possible exceptions). In this case, the job of the static type checker is just to check that raising the declared exception is possible at all. So:
def f() -> Raises[float, ZeroDivisionError]: # error
return 1.1
would be a type-check error, because the type checker can’t see a way for this exception to be raised.
However, this would be allowed:
def f(flag: bool) -> Raises[float, ZeroDivisionError]:
if flag:
g()
return 1.1
def g() -> Raises[float, ZeroDivisionError]:
return 1.0 / 0