1

In Python, specific POSIX error conditions do not have their separate exception types — they are distinguished by an attribute inside of the OSError exception object.

Let's imagine I'm performing a file operation (removing a possibly inexistent file over SFTP) and I want to ignore ENOENT, but still handle any other error or exception. Is it possible to do that more elegantly than as following?

try:
    action()
except OSError as e:
    if e.errno == errno.ENOENT:
        pass
    else:
        sophisticated_error_handling(e)
except e:
    sophisticated_error_handling(e)

I dislike this method because it involves repetition.

Note: there is no X-Y problem. The "action" is a library function and it cannot be told to ignore ENOENT.

init_js
  • 4,143
  • 2
  • 23
  • 53
intelfx
  • 2,386
  • 1
  • 19
  • 32
  • catch all errors and check the type in the if statement? Not sure how exactly, but pseudocode: `try: somecode except e: if e.type == OSError and e.errno == errno.ENOENT: pass / else: sophisticated()` – TessellatingHeckler Jul 28 '17 at 18:50
  • Example: https://repl.it/JnUQ/0 with a caveat of: https://stackoverflow.com/a/9824050/478656 – TessellatingHeckler Jul 28 '17 at 18:58

2 Answers2

2

There is a shorter, and more idiomatic way of achieving the same result than the code you propose.

You do try/catch for the error you anticipate, and then check the condition on the error object inside the except clause. The trick is to re-raise the same error object unchanged if the error object is not of the particular sub-type you were expecting.

import errno
try:
    action()
except IOError as ioe:
    if ioe.errno not in (errno.ENOENT,):
        # this re-raises the same error object.
        raise
    pass # ENOENT. that line is optional, but it makes it look
         # explicit and intentional to fall through the exception handler.

It's important that you re-raise the initial error with just raise, without parameters. You may be tempted re-raise with raise ioe instead of just raise on that line. Even though you would preserve the same error message, if you used raise ioe it would make the error's stack trace look as if the error happened on that line, and not inside action() where it really happened.

In your proposed code, your second exception handler (i.e. except e), even though it is syntactically valid, will not trigger. You have to specify except <type>:, or except <type> as <variable>:.

If you wanted to do additional sophisticated error handling, to ensure correctness, you could nest all of the previous try/catch in an outer try/except:

try:
    try:
        action()
    except IOError as ioe:
        if ioe.errno not in (errno.ENOENT,): raise
except IOError as any_other_ioerror:
    # this is reached in case you get other errno values
    sophisticated_error_handling()
except OtherExceptionTypeICareAbout as other:
    other_fancy_handler()

One thing you want to be mindful of, when nesting exception handlers like that, is that exception handlers can raise exceptions too. There's a tutorial on exception handling in the docs here, but you may find it a bit dense.

Method #2

If you're into meta-python tricks, you could consider an approach using the with statement. You could create a python context manager which absorbs particular types of errors.

# add this to your utilities
class absorb(object):
    def __init__(self, *codes):
        self.codes = codes
    def __exit__(self, exc_type, value, traceback):
        if hasattr(value, "errno") and getattr(value, "errno") in self.codes:
            return True # exception is suppressed
        return False # exception is re-raised
    def __enter__(self):
        return None

And then you can write simple code like:

# if ENOENT occurs during the block, abort the block, but do not raise.
with absorb(errno.ENOENT):
    delete_my_file_whether_it_exists_or_not()
    print("file deleted.") # reachable only if previous call returned

If you still want to have sophisticated error handling for other errno cases, you would include the above in a try/except, like so:

try:
    with absorb(errno.ENOENT): action()
    # the code here is reachable only if action() returns,
    # or if one of the suppressed/absorbed exceptions has been raised (i.e. ENOENT)
except IOError as any_other_ioerror:
    # any exception with errno != ENOENT will come here
    sophisticated_error_handling()

Note: you may also want to take a look at contextlib, which might eliminate some of the meta-gore for you.

init_js
  • 4,143
  • 2
  • 23
  • 53
  • Thanks, I know how to re-raise an exception. I actually specifically wanted to avoid double-try-catch, because it looks ugly. However, I love meta-programming and I do like your "Method #2" very much — thanks! – intelfx Sep 07 '18 at 17:16
-1

Next by the except just enter the error type!

test = [1, 2, 3, 4, 5, 6, 'aasd']
for  i in range(50):
   try:
      print(test[i])
except IndexError:
   pass
ThunderHorn
  • 1,975
  • 1
  • 20
  • 42