[Python-ideas] Type hints for functions with side-effects and for functions raising exceptions
Chris Angelico
rosuav at gmail.com
Wed Feb 20 05:42:17 EST 2019
On Wed, Feb 20, 2019 at 9:09 PM Ben Rudiak-Gould <benrudiak at gmail.com> wrote:
> That problem, of an inadvertently leaked implementation detail
> masquerading as a proper alternate return value, used to be a huge
> issue with StopIteration, causing bugs that were very hard to track
> down, until PEP 479 fixed it by translating StopIteration into
> RuntimeError when it crossed an abstraction boundary.
That's because a generator function conceptually has three ways to
provide data (yield, return, and raise), but mechanically, one of them
is implemented over the other ("return" is "raise StopIteration with a
value"). For other raised exceptions, this isn't a problem.
> I think converting exceptions to RuntimeErrors (keeping all original
> information, but bypassing catch blocks intended for specific
> exceptions) is the best option. (Well, second best after ML/Haskell.)
> But to make it work you probably need to support some sort of
> exception specification.
The trouble with that is that it makes refactoring very hard. You
can't create a helper function without knowing exactly what it might
be raising.
> I'm rambling. I suppose my points are:
>
> * Error handing is inherently hard, and all approaches do badly
> because it's hard and programmers hate it.
Well, yeah, no kidding. :)
> ... I was bitten several times by
> that StopIteration problem.
>
There's often a completely different approach that doesn't leak
StopIteration. One of my workmates started seeing RuntimeErrors, and
figured he'd need a try/except in this code:
def _generator(self, left, right):
while True:
yield self.operator(next(left), next(right))
But instead of messing with try/except, it's much simpler to use
something else - in this case, zip.
Generally, it's better to keep things simple rather than to complicate
them with new boundaries. Unless there's a good reason to prevent
leakage, I would just let exception handling do whatever it wants to.
But if you want to specifically say "this function will not raise
anything other than these specific exceptions", that can be done with
a decorator:
def raises(*exc):
"""Declare and enforce what a function will raise
The decorated function will not raise any Exception other than
the specified ones, or RuntimeError.
"""
def deco(func):
@functools.wraps(func)
def convert_exc(*a, **kw):
try: return func(*a, **kw)
except exc: raise
except Exception as e: raise RuntimeError from e
convert_exc.may_raise = exc # in case it's wanted
return convert_exc
return deco
This way, undecorated functions behave as normal, so refactoring isn't
impacted. But if you want the help of a declared list of exceptions,
you can have it.
@raises(ValueError, IndexError)
def frob(x):
...
ChrisA
More information about the Python-ideas
mailing list