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/