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.benja...@gmail.com>
wrote:

> On Fri, 25 Sep 2020 at 15:57, Paul Moore <p.f.mo...@gmail.com> wrote:
> >
> > On Fri, 25 Sep 2020 at 14:15, Chris Angelico <ros...@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-le...@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/
>
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@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/A6ACINJTZO6AQFWCPSNDV7GLTKBHW6F2/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to