[Python-ideas] Improving Catching Exceptions

Rob Cliffe rob.cliffe at btinternet.com
Sun Jun 25 20:06:43 EDT 2017



On 24/06/2017 11:03, Steven D'Aprano wrote:
> On Sat, Jun 24, 2017 at 01:02:55PM +1200, Greg Ewing wrote:
>
>> In any case, this doesn't address the issue raised by the OP,
>> which in this example is that if the implementation of
>> bah.__getitem__ calls something else that raises an IndexError,
>> there's no easy way to distinguish that from one raised by
>> bah.__getitem__ itself.
> I'm not convinced that's a meaningful distinction to make in general.
> Consider the difference between these two classes:
>
> class X:
>     def __getitem__(self, n):
>         if n < 0:
>              n += len(self)
>         if not 0 <= n < len(self):
>              raise IndexError
>         ...
>
> class Y:
>     def __getitem__(self, n):
>         self._validate(n)
>         ...
>      def _validate(self, n):
>         if n < 0:
>              n += len(self)
>         if not 0 <= n < len(self):
>              raise IndexError
>
>
> The difference is a mere difference of refactoring. Why should one of
> them be treated as "bah.__getitem__ raises itself" versus
> "bah.__getitem__ calls something which raises"? That's just an
> implementation detail.
>
> I think we're over-generalizing this problem. There's two actual issues
> here, and we shouldn't conflate them as the same problem:
>
> (1) People write buggy code based on invalid assumptions of what can and
> can't raise. E.g.:
>
>      try:
>          foo(baz[5])
>      except IndexError:
>          ... # assume baz[5] failed (but maybe foo can raise too?)
>
>
> (2) There's a *specific* problem with property where a bug in your
> getter or setter that raises AttributeError will be masked, appearing as
> if the property itself doesn't exist.
>
>
> In the case of (1), there's nothing Python the language can do to fix
> that. The solution is to write better code. Question your assumptions.
> Think carefully about your pre-conditions and post-conditions and
> invariants. Plan ahead. Read the documentation of foo before assuming
> it won't raise. In other words, be a better programmer.
>
> If only it were that easy :-(
>
> (Aside: I've been thinking for a long time that design by contract is a
> very useful feature to have. It should be possibly to set a contract
> that states that this function won't raise a certain exception, and if
> it does, treat it as a runtime error. But this is still at a very early
> point in my thinking.)
>
> Python libraries rarely give a definitive list of what exceptions
> functions can raise, so unless you wrote it yourself and know exactly
> what it can and cannot do, defensive coding suggests that you assume any
> function call might raise any exception at all:
>
>      try:
>          item = baz[5]
>      except IndexError:
>          ... # assume baz[5] failed
>      else:
>          foo(item)
>
>
> Can we fix that? Well, maybe we should re-consider the rejection of PEP
> 463 (exception-catching expressions).
>
> https://www.python.org/dev/peps/pep-0463/
I'm all in favour of that :-) but I don't see how it helps in this example:

     try:
         item = (baz[5] except IndexError: SomeSentinelValue)
     if item == SomeSentinelValue:
         ... # assume baz[5] failed
     else:
         foo(item)

is clunkier than the original version.  Or am I missing something? Only 
if the normal and exceptional cases could be handled the same way would 
it help:

     foo(baz[5] except IndexError: 0)

Rob Cliffe

>
>
> Maybe we need a better way to assert that a certain function won't raise
> a particular exception:
>
>      try:
>          item = bah[5]
>          without IndexError:
>              foo(item)
>      except IndexError:
>          ... # assume baz[5] failed
>
> (But how is that different from try...except...else?)
>
>
>
> In the case of (2), the masking of bugs inside property getters if they
> happen to raise AttributeError, I think the std lib can help with that.
> Perhaps a context manager or decorator (or both) that converts one
> exception to another?
>
> @property
> @bounce_exception(AttributeError, RuntimeError)
> def spam(self):
>       ...
>
>
> Now spam.__get__ cannot raise AttributeError, if it does, it will be
> converted to RuntimeError. If you need finer control over the code that
> is guarded use the context manager form:
>
> @property
> def spam(self):
>      with bounce_exception(AttributeError, RuntimeError):
>          # guarded
>          if condition:
>              ...
>      # not guarded
>      raise AttributeError('property spam doesn't exist yet')
>
>
>



More information about the Python-ideas mailing list