18

I'm aware of raise ... from None and have read How can I more easily suppress previous exceptions when I raise my own exception in response?.

However, how can I achieve that same effect (of suppressing the "During handling of the above exception, another exception occurred" message) without having control over the code that is executed from the except clause? I thought that sys.exc_clear() could be used for this, but that function doesn't exist in Python 3.

Why am I asking this? I have some simple caching code that looks like (simplified):

try:
    value = cache_dict[key]
except KeyError:
    value = some_api.get_the_value_via_web_service_call(key)
    cache_dict[key] = value

When there's an exception in the API call, the output will be something like this:

Traceback (most recent call last):
  File ..., line ..., in ...
KeyError: '...'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File ..., line ..., in ...
some_api.TheInterestingException: ...

This is misleading, as the original KeyError is not really an error at all. I could of course avoid the situation by changing the try/except (EAFP) into a test for the key's presence (LBYL) but that's not very Pythonic and less thread-friendly (not that the above is thread-safe as is, but that's beside the point).

It's unreasonable to expect all code in some_api to change their raise X to raise X from None (and it wouldn't even make sense in all cases). Is there a clean solution to avoid the unwanted exception chain in the error message?

(By the way, bonus question: the cache thing I used in the example is basically equivalent to cache_dict.setdefault(key, some_api.get_the_value_via_web_service_call(key)), if only the second argument to setdefault could be a callable that would only be called when the value needs to be set. Isn't there a better / canonical way to do it?)

Community
  • 1
  • 1
vashekcz
  • 561
  • 1
  • 4
  • 16
  • 1
    This `defaultdict` modification should be useful to you: http://stackoverflow.com/questions/2912231/is-there-a-clever-way-to-pass-the-key-to-defaultdicts-default-factory – sirfz May 14 '15 at 11:28

3 Answers3

12

You have a few options here.

First, a cleaner version of what orlp suggested:

try:
    value = cache_dict[key]
except KeyError:
    try:
        value = some_api.get_the_value(key)
    except Exception as e:
        raise e from None
    cache_dict[key] = value

For the second option, I'm assuming there's a return value hiding in there somewhere that you're not showing:

try:
    return cache_dict[key]
except KeyError:
    pass
value = cache_dict[key] = some_api.get_the_value(key)
return value

Third option, LBYL:

if key not in cache_dict:
    cache_dict[key] = some_api.get_the_value(key)
return cache_dict[key]

For the bonus question, define your own dict subclass that defines __missing__:

class MyCacheDict(dict):

    def __missing__(self, key):
        value = self[key] = some_api.get_the_value(key)
        return value

Hope this helps!

Zachary Ware
  • 325
  • 2
  • 8
  • 2
    Thanks. Your first suggestion has the problem that 1) it potentially suppresses more than it should (perhaps there WAS a useful context within the code of the API that should be listed in the tracebank), and 2) it obscures the source of the exception (instead of seeing a module from the API as the last line in the traceback, the user will now see my code, and the API will be the last-but-one line). Still, in the absence of sys.exc_clear(), it's my preferred solution of all those I have seen so far. – vashekcz Oct 11 '16 at 12:36
  • Your point about suppressing possibly useful context is definitely valid, and for that reason that suggestion probably shouldn't be used. However, your point about obscuring the source is not valid: the exact same exception, traceback and all, is re-raised, just without its context. I would much rather use a dict subclass with `__missing__` or the LBYL option, personally. – Zachary Ware Oct 12 '16 at 16:01
  • I find it astounding that there's no way to _prevent_ the `__context__` assignment; trying to remove it later is inherently unclean, and having to rearrange everything to be outside an `except` is gratuitously awkward. – Davis Herring Nov 14 '22 at 03:55
0

You can try suppressing the context yourself:

try:
    value = cache_dict[key]
except KeyError:
    try:
        value = some_api.get_the_value_via_web_service_call(key)
    except Exception as e:
        e.__context__ = None
        raise

    cache_dict[key] = value
orlp
  • 112,504
  • 36
  • 218
  • 315
0

Here is a version of @Zachary's second option whose use is a little simpler. First, a helper subclass of dict which returns a sentinal value on a "miss" rather than throwing an exception:

class DictCache(dict):
    def __missing__(self, key):
        return self

then in use:

cache = DictCache()
...
value = cache[K]
if value is cache:
    value = cache[K] = some_expensive_call(K)

Notice the use of "is" rather than "==" to ensure there is no collision with a valid entry.

If the thing being assigned to is a simple variable (i.e. "value" rather than an attribute of another variable "x.value"), you can even write just 2 lines:

if (value := cache[K]) is cache:
    value = cache[K] = some_expensive_call(K)
Shaheed Haque
  • 644
  • 5
  • 14