I strongly disagree that it's useless to document which Exceptions a function could raise; even in Python (which, for a few reasons, is not a language that's considered for safety-critical application).

In Python, it is common practice to - at a high level in the call stack - trap Exceptions that can occur anywhere like KeyboardInterrupt and MemoryError (and separately specify signal handler callbacks).

A high-level catchall (except for KeyboardInterrupt) and restart may be the best way to handle exceptions in Python.

Safe coding styles (in other languages) do specify that *there may not be any unhandled exceptions*.
Other languages made the specific decision to omit exceptions entirely:
developers should return `retval, err := func(arg)` and handle every value of err.
Python has Exceptions and it's helpful to document what exceptions a function might `raise` (even though it is possible to parse the AST to find the `raise` statements within a callable and any callables it may or may not handle). There are a few useful ideas for checking Exception annotations at compile-time in this thread.

https://en.wikipedia.org/wiki/Exception_handling#Static_checking_of_exceptions
https://en.wikipedia.org/wiki/Exception_handling#Dynamic_checking_of_exceptions

We could pick one or more of the software safety standards listed here and quote and cite our favs:
https://awesome-safety-critical.readthedocs.io/en/latest/#software-safety-standards

## Exception docstrings
You can specify Exceptions in all formats of sphinx docstrings:

### Sphinx-style docstrings:

```python
"""
:raises: AttributeError: The ``Raises`` section is a list of all exceptions
            that are relevant to the interface.
:raises: ValueError: If `param2` is equal to `param1`.
"""
```

### Google-style docstrings:
```python
"""
    Raises:
        AttributeError: The ``Raises`` section is a list of all exceptions
            that are relevant to the interface.
        ValueError: If `param2` is equal to `param1`.
"""
```

###Numpy-style docstrings:
```python
"""
    Raises
    ------
    AttributeError
        The ``Raises`` section is a list of all exceptions
        that are relevant to the interface.
    ValueError
        If `param2` is equal to `param1`.
"""
```

https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html#example-google
https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_numpy.html#example-numpy

## Design-by-contracts
FWICS, neither e.g. icontract nor zope.interface support Exception contracts. How could that work.


## Awesome-safety-critical
https://awesome-safety-critical.readthedocs.io/en/latest/#software-safety-standards


On Fri, Sep 25, 2020 at 12:34 PM Oscar Benjamin <oscar.j.benjamin@gmail.com> wrote:
On Fri, 25 Sep 2020 at 15:57, Paul Moore <p.f.moore@gmail.com> wrote:
>
> On Fri, 25 Sep 2020 at 14:15, Chris Angelico <rosuav@gmail.com> wrote:
>
> > Why? Do you really think you can enumerate EVERY possible way that
> > something might fail?
>
> Rust does a surprisingly good job of that, actually. But the point is
> that Python is not Rust, and the infrastructure Rust uses to allow it
> to manage code safety is baked into the language core at a very
> fundamental level.
>
> Enumerating the exceptions that a piece of code can raise is
> impractical and unhelpful in Python. But that may not be immediately
> obvious to someone coming from a different language. That's why it's
> important to understand Python properly before proposing new features
> that work in other languages. (I don't think that's what the OP is
> doing here, to be clear, but the discussion is drifting in that
> direction, with Rust's Result type having been mentioned).
>
> **In Python**, writing code from the perspective of "what can I handle
> at this point" is the right approach. Deferring unexpected exceptions
> to your caller is the default behaviour, and results in a clean,
> natural style *for Python*. The proposal here is basically in direct
> contradiction to that style.

I do agree but maybe that suggests a different role for annotated
exceptions in Python. Rather than attempting to enumerate all possible
exceptions annotations could be used to document in a statically
analysable way what the "expected" exceptions are. A type checker
could use those to check whether a caller is handling the *expected*
exceptions rather than to verify that the list of *all* exceptions
possibly raised is exhaustive.

Consider an example:

def inverse(M: Matrix) -> Matrix: raises(NotInvertibleError)
    if determinant(M) == 0:
        raise NotInvertibleError
    rows, cols = M.shape
    for i in range(rows):
        for j in range(cols):
            ...

Here the function is expected to raise NotInvertibleError for some
inputs. It is also possible that the subsequent code could raise an
exception e.g. AttributeError, TypeError etc and it's not necessarily
possible to enumerate or exhaustively rule out what those
possibilities might be. If we wanted to annotate this with
raises(NotInvertibleError) then it would be very hard or perhaps
entirely impossible for a type checker to verify that no other
exception can be raised. Or maybe even the type checker could easily
come up with a large list of possibilities that you would never want
to annotate your code with. Maybe that's not what the purpose of the
annotation is though.

What the type checker can do is check whether a caller of this
function handles NotInvertibleError after seeing the *explicit* type
hint. A function that calls inverse without catching the exception can
also be considered as raises(NotInvertibleError). You might want to
enforce in your codebase that the caller should catch and suppress the
expected exception or should itself have a compatible raises
annotation indicating that it can also be expected to raise the same
exception e.g. either of these is fine:

def some_calc(M: Matrix): raises(NotInvertibleError)
    A = inverse(M)
    ...

def some_calc(M: Matrix):
    try:
        A = inverse(M)
    except NotInvertibleError
        # do something else
    ...

Perhaps rather than requiring all exceptions to be annotated
everywhere you could allow the raises type hints to propagate
implicitly and only verify them where there is another explicit type
hint:

def some_calc(M):
    # no hint but checker infers this raises NotInvertibleError
    A = inverse(M)

def other_func(M): raises(ZeroError)
    # checker gives an error for this
    # because the raises should include NotInvertibleError
    B = some_calc(M)

You could then have an explicit hint for the type checker to say that
a function is not expected to raise any exceptions maybe like this:

def main(args): raises(None)
    ...

The intent of this would be that the type checker could then follow
the chain of all functions called by main to verify that any
exceptions that were expected to raise had been handled somewhere.
This wouldn't verify all of the exceptions that could possibly be
raised by any line of code. It could verify that for those exceptions
that have been explicitly annotated.


Oscar
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-leave@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/L2YK75C7XSFWOJLM6ROAI3ZVAY2WE5GZ/
Code of Conduct: http://python.org/psf/codeofconduct/