[Python-ideas] Type hints for functions with side-effects and for functions raising exceptions

Kyle Lahnakoski klahnakoski at mozilla.com
Fri Feb 22 17:13:17 EST 2019


On 2019-02-21 03:09, Christopher Barker wrote:
>
> But yes, there is no (easy) way to distinguish an Exception raised by
the function you called, and one raised somewhere deeper that.
>
> And I have been bitten by that more than once. It makes "Easier to ask
forgiveness than permission" kind of tricky.

> And Exception handling is messy -- the point made by the OP, I'm not
sure there's a better way to do it.


It seems to me that exception *classes* are hardly ever required. If two
methods raise the same exception type, then I contend that they are
actually throwing two different types of errors (as evidenced by the
difference in the message). Instead, I suggest every method emits its
own exception type.

By demanding support for a plethora of exception types, at least as many
as there are methods, then we may have a different way of looking at
exceptions: The first conclusion is, it is too expensive to make classes
for every exception type, rather, exception type should be defined by
the message.


Exception handling is messy. I find most of my Python methods look like:

    | def my_method():
    |     try:
    |         # do something
    |     except Exception as e:
    |         error("some description", cause=e)

I am not using Python3 everywhere yet, so maybe it should read like:

    | def my_method():
    |     try:
    |         # do something
    |     except Exception as e:
    |         raise Exception("some description") from e


Let me call this pattern the Catch-It-Name-It-Chain-It-Raise-It (CNCR)
pattern  

There are a few reasons for this.

1. I can add runtime values to the exception so I get a better sense of
the program state without going to the debugger:  `error("some
description", {"url": url}, cause=e)`
2. I prevent exception leakage; I have no idea the diversity of
exceptions my code can raise, so I CNCR.
3. Every exception is it's own unique type; I can switch on the message
if I want (I rarely do this, but it happens, see below)

Can Python provide better support for the CNCR pattern? If it is
lightweight enough, maybe people will use it, and then we can say
something useful about the (restricted) range of exceptions coming from
a method:

A context manager, a `with` block, could do this:  

    | def my_method():
    |     with Explanation("some description"):
    |         # do something

I propose a simple line, which effectively defines a try block to the
end of the current code block. Call it the `on-raises` syntax:

    | def my_method():
    |     on Exception raises "some description"
    |     # do something

It is better than the `with` block because:

* it does not put code in the happy path
* it has less indentation
* we can conclude "some description" is the only exception raised by
this method  

The `on-raises` need not start at the beginning of a code block, but
then we can say less about what exceptions come out of `my_method`:

    | def my_method():
    |     # no exception checks here
    |     on Exception raises "some description"
    |     # do something

Since `on-raises` can be used in any code block, we can save some
indentation. Instead of

    | def my_method():
    |     with some_file:
    |         try:
    |             # do something
    |         except Exception as e:
    |             raise Exception("some description") from e

we have

    | def my_method():
    |     with some_file:
    |         on Exception raises "some description"
    |         # do something

of course we can have nested `on-raises`,

    | def my_method():
    |     on Exception raises "bigger problem"
    |     with some_file:
    |         on Exception raises "some description"
    |         # do something

Plus we know only "bigger problem" exceptions can be raised.

The above is the same as:
    | def my_method():
    |     try:
    |         with some_file:
    |             try:
    |                 # do something
    |             except Exception as e:
    |                 raise Exception("some description") from e
    |     except Exception as e:
    |         raise Exception("bigger problem") from e

in the rare case we actually want to deal with an exception, we revert
back to trusty old try/except:

    | def my_other_method():
    |     on Exception raises "some other description"
    |     try:
    |         my_method()
    |     except "some description" as e:
    |         # I know how to handle this case
    |         return

which is the same as:

    | def my_other_method():
    |     try:
    |         my_method()
    |     except Exception as e:
    |         if "some description" in e:
    |             # I know how to handle this case
    |             return
    |         error("some other description", cause=e)


More information about the Python-ideas mailing list