typing icon indicating copy to clipboard operation
typing copied to clipboard

Annotating functions which don't raise exceptions

Open pfalcon opened this issue 5 years ago • 7 comments

I would find it interesting to be able to annotate functions which never throw exceptions. "Never" can be either in the absolute sense, or in the sense of "checked exceptions", with some subset of exception designated as "unchecked" (details of this designation are perhaps up to the actual tool to process annotations).

Example of a function which absolutely never raises exceptions (well, at least per language semantics, particular implementations might still have means to break with some implementation-related exceptions):

def foo(l):
    if isinstance(l, list) and len(l) > 0:
        return l[0]

Example of a function which may throw (unchecked) exception if API contract is violated:

def foo(l: list):
    if len(l) > 0:
        return l[0]

So, hopefully these examples show that the notion does exist in Python.

Now the question how to annotate it. Following the existing NoReturn, it would be called NoRaise or NoThrow. "nothrow" terminology if familiar from C++ (and it seems to be replaced even there), so perhaps sticking with native Python terminology makes sense (but "throw" is native to Python too, re: exception handling with generators).

More interesting question is where to put that annotation. Taking the example above, a natural annotation would be:

def foo(l) -> Optional(Any), NoRaise:

And I was quite surprised that the usual "implicit tuple" syntax rule doesn't apply here, and the above is SyntaxError as of CPy3.7:

Python 3.7.1 (default, Oct 22 2018, 11:21:55) 
[GCC 8.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def foo() -> list, int:
  File "<stdin>", line 1
    def foo() -> list, int:
                     ^
SyntaxError: invalid syntax

I wonder if that warrants a separate issue report. And whether it was a cunning design choice mere mortals need to decipher, because that seems too obvious thing to overlook with an implicit tuple syntax, again (neither problematic from forma grammar perspective, even in LL it's "->" expr ("," expr)* ":", which is trivial, Python has more complex grammar rules in many places.

So, all in all, now it would need to be written as:

def foo(l) -> (Optional(Any), NoRaise):

Which is of course not as pretty as without parens.

pfalcon avatar Jan 07 '19 08:01 pfalcon

This is related to https://github.com/python/typing/issues/71 . In a sense, it's complement of it: there, people want to annotate which exceptions are thrown. Supposedly, when these annotations are full and complete, functions which aren't annotated to raise any exception, are those which don't raise them. In reality of course, that level of coverage is not realistically achievable, so instead of explicitly marking all exceptions raised, thin RFC proposes to allow to explicitly mark those functions which don't raise exceptions. More discussion of this approach: https://github.com/python/typing/issues/71#issuecomment-451846224

pfalcon avatar Jan 07 '19 08:01 pfalcon

I had this idea in mind for some time, and the discussion in https://mail.python.org/pipermail/python-dev/2019-January/155998.html only gives an example that "nothrow" property is important to the Python language, whether those in powers of it admit, or not.

To recap, the talk in that thread (and with reference to the official docs at https://docs.python.org/3/reference/compound_stmts.html#try) is that

except E as N:
    body

is compiled as

except E as N:
    try:
        body
    finally:
        N = None
        del N

The reason why there're 2 statements "N = None; del N", is exactly for that (compiler-generated) code to have no-throw property - even if "body" (containing arbitrary user code) has del N statement itself.

But the reason the exception handler body needs to be wrapped into extra try-finally in the first place, is from the reservations that it may throw exceptions! If we'd know that it doesn't, we could avoid that overhead. And note that it doesn't even apply to "native code generation", but to bytecode generation, as done e.g. by CPython.

Of course, I don't call for CPython itself to do such an optimization. But there're many Python language implementations which try to advance performance state of the start for the language, and setting a common ground for them to annotate nothrow/NoRaise functions is worthy a task IMHO. With that idea I submit this ticket.

pfalcon avatar Jan 07 '19 08:01 pfalcon

I would find it interesting to be able to annotate functions which never throw exceptions.

What exactly do you want? Do you just want to have a syntax to attach some extra info to (return) types that would be ignored by type checkers but may be used by some other tools? If yes, then this is essentially a duplicate of https://github.com/python/typing/issues/600. With that you can write:

def func() -> Annotated[int, NoRaise]:
    ...

ilevkivskyi avatar Jan 07 '19 17:01 ilevkivskyi

Thanks for reference to #600. I guess I could crash there with alternative syntax ideas ;-).

Beyond syntax for multiple annotations, it's also about choosing a specific annotation symbol for "no-throw" case. (I understand it unlikely to be added to "typing" module any type soon, so question is "if it would be added, what would it be", and python/typing is a kind of crossroads among different annotations projects, that's why I posted it here).

pfalcon avatar Jan 09 '19 21:01 pfalcon

The choice of name is up to the team/tool that is going to support it, other type checkers will just ignore it. We can keep this open for a while to see who else is interested in this feature.

ilevkivskyi avatar Jan 10 '19 10:01 ilevkivskyi

+1

SemMulder avatar Jun 11 '19 08:06 SemMulder

Perhaps an approach to consider interesting approach is similar to what Swift does

def can_throw_errors(): ThrowsError[String]

rmzr7 avatar Aug 09 '21 19:08 rmzr7