Which is worse - duplicated code or double try/except? - python

I have a situation where I want to do multiple things while handling an exception. Since I want to make this about the general case, I'll translate my specific case into some more general language.
When I have an exception in this piece of code, I want to:
Always perform a rollback-style operation
If it is an
application specific exception, I want to perform some logging and swallow the exception.
So I can think of two ways to solve it, both ugly:
# Method nested-try/except block
try:
try:
do_things()
except:
rollback()
raise
except SpecificException as err:
do_advanced_logging(err)
return
# Method Duplicate Code
try:
do_things()
except SpecificException as err:
rollback()
do_advanced_logging(err)
return
except:
rollback()
raise
Both will have the same behaviour.
I'm tending towards the nested try/except solution myself. While it might be slightly slower, I don't think the speed difference is relevant here - at the very least not for my specific case. Duplication of code is something I want to avoid also because my rollback() statement is slightly more involved that just a database rollback, even if it has the exact same purpose (it involves a web-API).
Is there a third option I haven't spotted that is better? Or is the duplicate code method better? Please note that the rollback() functionality is already factored out as much as possible, but still contains a function call and three arguments which includes a single hardcoded string. Since this string is unique, there's no reason to make it a named constant.

How about checking the exception instance type in code?
# Method .. No Duplicate Code
try:
do_things()
except Exception as e:
rollback()
if isinstance(e, SpecificException):
do_advanced_logging(e)
return
raise

how about putting the rollback in a finally clause? something like:
do_rollback = True
try:
do_things()
do_rollback = False
except SpecificException as err:
do_advanced_logging(err)
finally:
if do_rollback:
rollback()
an alternative is to use an else clause, which would let you do more in the non-exceptional case and not have exceptions all caught in the same place:
do_rollback = True
try:
do_things()
except SpecificException as err:
do_advanced_logging(err)
else:
record_success()
do_rollback = False
finally:
if do_rollback:
rollback()
is useful when record_success can raise a SpecificException, but you don't want to do_advanced_logging

You could write a context manager:
import random
class SpecificException(Exception):
pass
def do_things(wot=None):
print("in do_things, wot = {}".format(wot))
if wot:
raise wot("test")
def rollback():
print("rollback")
def do_advance_logging(exc_type, exc_val, traceback):
print("logging got {} ('{}')".format(exc_type, exc_val))
class rollback_on_error(object):
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, traceback):
# always rollback
rollback()
# log and swallow specific exceptions
if exc_type and issubclass(exc_type, SpecificException):
do_advance_logging(exc_type, exc_val, traceback)
return True
# propagate other exceptions
return False
def test():
try:
with rollback_on_error():
do_things(ValueError)
except Exception as e:
print("expected ValueError, got '{}'".format(type(e)))
else:
print("oops, should have caught a ValueError")
try:
with rollback_on_error():
do_things(SpecificException)
except Exception as e:
print("oops, didn't expect exception '{}' here".format(e))
else:
print("ok, no exception")
try:
with rollback_on_error():
do_things(None)
except Exception as e:
print("oops, didn't expect exception '{}' here".format(e))
else:
print("ok, no exception")
if __name__ == "__main__":
test()
But unless you have dozen occurrences of this pattern, I'd rather stick to the very obvious and perfectly pythonic solutions - either nested exceptions handlers or explicit typecheck (isinstance) in the except clause.

Related

class '_mysql_exceptions.DataError': How to check a variable type in an if condition

I need to return update/insert result, from the database class, back to the calling class to differentiate
between success and error.
An update/insert returns <type long'> while a database error returns
<class '_mysql_exceptions.DataError'>.
Since I am not sure about the return type during a success that it would always be a long type, I am checking for type class.
And, I couldn't do it. I tried these:
try:
x = cursor.execute(q, d)
conn.commit()
return x #Return this to the calling class
except MySQLdb.Error, e:
return e #Return this to the calling class
if isinstance(e, class): #Doesn't work
if issubclass(e, _mysql_exceptions): #Doesn't work
How do I check the type of e here?
If I am doing it all wrong, please suggest something nice, thanks.
The issue is that isinstance(obj, class) is not valid syntax, and _mysql_exceptions is a module, not an exception type, which raises a TypeError. To explicitly check an exception type, you can catch each individually:
from _mysql.exceptions import DataError, SomeOtherError, ...
from requests import HTTPError # as an example of a different error
try:
x = cursor.execute(q, d)
conn.commit()
except DataError as e:
# do something
except SomeOtherError as e:
# do something else
except HTTPError as e:
# your connection is broken
# maybe raise from e?
You need to catch that explicit error type, then you don't need to do if isinstance. Start with no exception handling at all, this will lead you to the exceptions that you do need to handle, and anything else should be considered unexpected and should cause the application to either crash or propagate some helpful error message to let you know something bad happened:
try:
some_function()
except ValueError as e:
# this is expected, and is handled accordingly
handle_expected_error()
# This is optional, normally a bare exception block is considered bad practice,
# but can allow your application to continue functioning while raising some
# helpful error so this isn't suppressed
except Exception as e:
# this is not expected, I'm going to propagate this error
# up to be obvious what happened
handle_unexpected_error()
#or
raise from e
Edit: What if I want a calling class to handle the exception?
Reasonable, and I would lean on catching the exception. Instead of handling the exception, I would allow the function to just raise the exception and handle it in the calling class. As a really simple example:
class MyClass:
def __init__(self, conn, cursor):
self.conn = conn
self.cursor = cursor
def some_function(self):
# This raises an error, note I'm not handling it here
x = self.cursor.execute()
self.conn.commit()
return x
def main_function(self):
try:
x = self.some_function()
except DataError as e:
handle_exception()
# unexpected, handle this here
except Exception as e:
raise from e
# or do something else

Count exception thrown in Python [duplicate]

Is it possible to tell if there was an exception once you're in the finally clause? Something like:
try:
funky code
finally:
if ???:
print('the funky code raised')
I'm looking to make something like this more DRY:
try:
funky code
except HandleThis:
# handle it
raised = True
except DontHandleThis:
raised = True
raise
else:
raised = False
finally:
logger.info('funky code raised %s', raised)
I don't like that it requires to catch an exception, which you don't intend to handle, just to set a flag.
Since some comments are asking for less "M" in the MCVE, here is some more background on the use-case. The actual problem is about escalation of logging levels.
The funky code is third party and can't be changed.
The failure exception and stack trace does not contain any useful diagnostic information, so using logger.exception in an except block is not helpful here.
If the funky code raised then some information which I need to see has already been logged, at level DEBUG. We do not and can not handle the error, but want to escalate the DEBUG logging because the information needed is in there.
The funky code does not raise, most of the time. I don't want to escalate logging levels for the general case, because it is too verbose.
Hence, the code runs under a log capture context (which sets up custom handlers to intercept log records) and some debug info gets re-logged retrospectively:
try:
with LogCapture() as log:
funky_code() # <-- third party badness
finally:
# log events are buffered in memory. if there was an exception,
# emit everything that was captured at a WARNING level
for record in log.captured:
if <there was an exception>:
log_fn = mylogger.warning
else:
log_fn = getattr(mylogger, record.levelname.lower())
log_fn(record.msg, record.args)
Using a contextmanager
You could use a custom contextmanager, for example:
class DidWeRaise:
__slots__ = ('exception_happened', ) # instances will take less memory
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# If no exception happened the `exc_type` is None
self.exception_happened = exc_type is not None
And then use that inside the try:
try:
with DidWeRaise() as error_state:
# funky code
finally:
if error_state.exception_happened:
print('the funky code raised')
It's still an additional variable but it's probably a lot easier to reuse if you want to use it in multiple places. And you don't need to toggle it yourself.
Using a variable
In case you don't want the contextmanager I would reverse the logic of the trigger and toggle it only in case no exception has happened. That way you don't need an except case for exceptions that you don't want to handle. The most appropriate place would be the else clause that is entered in case the try didn't threw an exception:
exception_happened = True
try:
# funky code
except HandleThis:
# handle this kind of exception
else:
exception_happened = False
finally:
if exception_happened:
print('the funky code raised')
And as already pointed out instead of having a "toggle" variable you could replace it (in this case) with the desired logging function:
mylog = mylogger.WARNING
try:
with LogCapture() as log:
funky_code()
except HandleThis:
# handle this kind of exception
else:
# In case absolutely no exception was thrown in the try we can log on debug level
mylog = mylogger.DEBUG
finally:
for record in log.captured:
mylog(record.msg, record.args)
Of course it would also work if you put it at the end of your try (as other answers here suggested) but I prefer the else clause because it has more meaning ("that code is meant to be executed only if there was no exception in the try block") and may be easier to maintain in the long run. Although it's still more to maintain than the context manager because the variable is set and toggled in different places.
Using sys.exc_info (works only for unhandled exceptions)
The last approach I want to mention is probably not useful for you but maybe useful for future readers who only want to know if there's an unhandled exception (an exception that was not caught in any except block or has been raised inside an except block). In that case you can use sys.exc_info:
import sys
try:
# funky code
except HandleThis:
pass
finally:
if sys.exc_info()[0] is not None:
# only entered if there's an *unhandled* exception, e.g. NOT a HandleThis exception
print('funky code raised')
raised = True
try:
funky code
raised = False
except HandleThis:
# handle it
finally:
logger.info('funky code raised %s', raised)
Given the additional background information added to the question about selecting a log level, this seems very easily adapted to the intended use-case:
mylog = WARNING
try:
funky code
mylog = DEBUG
except HandleThis:
# handle it
finally:
mylog(...)
You can easily assign your caught exception to a variable and use it in the finally block, eg:
>>> x = 1
>>> error = None
>>> try:
... x.foo()
... except Exception as e:
... error = e
... finally:
... if error is not None:
... print(error)
...
'int' object has no attribute 'foo'
Okay, so what it sounds like you actually just want to either modify your existing context manager, or use a similar approach: logbook actually has something called a FingersCrossedHandler that would do exactly what you want. But you could do it yourself, like:
#contextmanager
def LogCapture():
# your existing buffer code here
level = logging.WARN
try:
yield
except UselessException:
level = logging.DEBUG
raise # Or don't, if you just want it to go away
finally:
# emit logs here
Original Response
You're thinking about this a bit sideways.
You do intend to handle the exception - you're handling it by setting a flag. Maybe you don't care about anything else (which seems like a bad idea), but if you care about doing something when an exception is raised, then you want to be explicit about it.
The fact that you're setting a variable, but you want the exception to continue on means that what you really want is to raise your own specific exception, from the exception that was raised:
class MyPkgException(Exception): pass
class MyError(PyPkgException): pass # If there's another exception type, you can also inherit from that
def do_the_badness():
try:
raise FileNotFoundError('Or some other code that raises an error')
except FileNotFoundError as e:
raise MyError('File was not found, doh!') from e
finally:
do_some_cleanup()
try:
do_the_badness()
except MyError as e:
print('The error? Yeah, it happened')
This solves:
Explicitly handling the exception(s) that you're looking to handle
Making the stack traces and original exceptions available
Allowing your code that's going to handle the original exception somewhere else to handle your exception that's thrown
Allowing some top-level exception handling code to just catch MyPkgException to catch all of your exceptions so it can log something and exit with a nice status instead of an ugly stack trace
If it was me, I'd do a little re-ordering of your code.
raised = False
try:
# funky code
except HandleThis:
# handle it
raised = True
except Exception as ex:
# Don't Handle This
raise ex
finally:
if raised:
logger.info('funky code was raised')
I've placed the raised boolean assignment outside of the try statement to ensure scope and made the final except statement a general exception handler for exceptions that you don't want to handle.
This style determines if your code failed. Another approach might me to determine when your code succeeds.
success = False
try:
# funky code
success = True
except HandleThis:
# handle it
pass
except Exception as ex:
# Don't Handle This
raise ex
finally:
if success:
logger.info('funky code was successful')
else:
logger.info('funky code was raised')
If exception happened --> Put this logic in the exception block(s).
If exception did not happen --> Put this logic in the try block after the point in code where the exception can occur.
Finally blocks should be reserved for "cleanup actions," according to the Python language reference. When finally is specified the interpreter proceeds in the except case as follows: Exception is saved, then the finally block is executed first, then lastly the Exception is raised.

python: Exception flow: Continue to down catch block after catching?

I am curious if there is a way in python to continue on within try/catch block, after you catch an exception, look at its properties, and if not relevant, then continue down the stack.
try:
# Code
except AppleError as apple_ex:
# look at 'apple_ex.error_code' error body, and if not relevant,
# continue on to next down the catch block...
# In other words, proceed to except BananaError and so on down.
except BananaError as banana_ex:
# ...
except Exception as ex:
# ...
That is not how exceptions are handled in Python. When you raise an exception in a try block, if you handle catching it in the except, it will fall inside that block, but will not continue to the next except at that same level. Observe this functional example:
try:
raise AttributeError()
except AttributeError:
raise TypeError()
except TypeError:
print("it got caught") # will not catch the TypeError raised above
So, in your try, we raise an AttributeError, we catch it, and then raise a TypeError inside catching the AttributeError.
The except TypeError will not catch that TypeError.
Based on how you are explaining your problem, you need to rethink how you are handling your exceptions and see if you can determine the handling of errors somewhere else, and raise the error there.
For example:
def some_func():
try:
thing()
except SomeException:
# analyze the exception here and raise the error you *should* raise
if apple_error_thing:
raise AppleError
elif banana_error_thing:
raise BananaError
else:
raise UnknownException
def your_func():
try:
some_func()
except AppleError as e:
print('Apple')
except BananaError as e:
print('Banana')
except UnknownException as e:
print('Unknown')
An AppleError is still an AppleError and not a BananaError, even if error_code is not relevant, so it makes no sense to fall through to BananaError.
You could instead define specific errors for your different error codes:
GRANNY_SMITH_ERROR = 1
MACINTOSH_ERROR = 2
class AppleError(Exception):
def __init__(self, error_code, *args):
super(AppleError, self).__init__(*args)
self.error_code = error_code
class GrannySmithError(AppleError):
def __init__(self, *args):
super(GrannySmithError, self).__init__(GRANNY_SMITH_ERROR, *args)
class MacintoshError(AppleError):
def __init__(self, *args):
super(MacintoshError, self).__init__(MACINTOSH_ERROR, *args)
Then you can try to match the specific error:
try: raise MacintoshError()
except MacintoshError as exc: print("mac")
except GrannySmithError as exc: print("granny smith")
If you do not care to distinguish between different types of apple errors, you can still trap all apple errors:
try: raise MacintoshError()
except AppleError as exc: print("generic apple")
You can combine these, for example, only doing special processing for GrannySmith, not for Macintosh:
try: raise MacintoshError()
except GrannySmithError as exc: print("granny smith")
except AppleError as exc: print("generic apple")
The important thing is to list the errors from most specific to least specific. If you test for AppleError before GrannySmithError, then it will never enter the GrannySmith block.
No, that isn't possible. After the exception is handled by the inner except it doesn't have the ability to get handled by the outer except:
From the docs on the try statement:
When the end of this block is reached, execution continues normally after the entire try statement. (This means that if two nested handlers exist for the same exception, and the exception occurs in the try clause of the inner handler, the outer handler will not handle the exception.)
In short your only solution might be to have another handler at an outer level and re-raise the exception in the inner handler, that is:
try:
try:
raise ZeroDivisionError
except ZeroDivisionError as e:
print("caught")
raise ZeroDivisionError
except ZeroDivisionError as f:
print("caught")
Now the nested except raises an exception which is consequently caught by a similar handler.

Always perform finally block except for one exception

I have a try:finally block that must execute always (exception or not) unless a specific exception occurs. For the sake of argument let's say it's a ValueError, so I'm asking if I can implement:
try:
stuff()
except Exception as e:
if type(e) is ValueError: raise
#do important stuff
raise
#do important stuff
in a more elegant fashion to skip copy-pasting #importantstuff. If I ruled Python it would look something like:
try:
stuff()
finally except ValueError:
#do important stuff
Putting #importantstuff in a function is not an answer, but not possible is.
If you need finally to skip things in specific conditions, you'll need to use an explicit flag:
do_final_stuff = True
try:
# ...
except ValueError:
do_final_stuff = False
raise
finally:
if do_final_stuff:
# ...
You could also use a context manager here, to clean up afterwards. A context manager is passed the current active exception if there is one:
class MyContextManager:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, tb):
if exc_type is not ValueError:
# do cleanup
with MyContextManager():
# ...

python continue with except if condition not met

I often find myself wanting to do something like this, I have something wrapped in try excepts like this
item= get_item()
try:
do_work(item)
except SomeError as err:
if err.code == 123:
do_something(item)
else:
# Actually I don't want to do something with this error code... I want to handle in 'except'
except:
put_back(item)
raise
Is there a way to raise into the except block below from the else? (a continue would be nice) I end up doing something like the following which isn't as clean
item= get_item()
try:
try:
do_work(item)
except SomeError as err:
if err.code == 123:
do_something(item)
else:
raise
except:
put_back(item)
raise
Is there anyway to do that?
If you are using a recent enough python version (2.5 and up), you should switch to using a context manager instead:
class WorkItemContextManager(object):
def __enter__(self):
self.item = get_item()
return self.item
def __exit__(self, exc_type, exc_value, tb):
if exc_type is not None:
if exc_type is SomeError and exc_value.code == 123:
do_something(self.item)
return True # Exception handled
put_back(self.item)
Then:
with WorkItemContextManager() as item:
do_work(item)
The __exit__ method can return True if an exception has been handled; returning None will instead re-raise any exceptions raised in the with block.
If not, you are looking for a finally block instead:
item = get_item()
try:
do_work(item)
item = None
except SomeError as err:
if err.code == 123:
do_something(item)
item = None
finally:
if item is not None:
put_back(item)
The finally suite is guaranteed to be executed when the try: suite completes, or an exception has occurred. By setting item to None you basically tell the finally suite everything completed just fine, no need to put it back.
The finally handler takes over from your blanket except handler. If there has been an exception in do_work, item will not be set to None. If the SomeError handler doesn't catch the exception, or err.code is not 123, item will also not be set to None, and thus the put_back(item) method is executed.
My suggestion would be to create a function (or series of functions) that wraps the method throwing errors which you'd like to control. Something like...
def wrapper(arg):
try:
do_work(arg)
except SomeError as e:
if e.code == 123:
do_something(item)
# Other possible cleanup code
else:
raise
...then, when you want to call it...
try:
wrapper(arg)
except SomeError as e:
put_back(arg)
Context managers are excellent, but for some simple cases where you won't be reusing the logic anywhere else, they can be a bit heavy.
Instead of trying to have multiple except blocks, you can just test the exception inside a single except block:
item= get_item()
try:
do_work(item)
except Exception as err:
if isinstance(err, SomeError) and err.code == 123:
do_something(item)
else:
put_back(item)
raise
Note that this is pretty much what a context manager's __exit__ method ends up looking like, anyway.
Be wary that code that really belongs in finally doesn't end up here.
It's good to keep in mind what try-except flow is for, and one of their advantages is that they remove the need for status variables and status checks like
if not foo:
# do something
Also, an Exception class should represent a specific kind of error. If you need to make further decisions about the kind of error in an except block, it's a good sign that the class isn't specific enough to represent the program state. Your best bet is to subclass SomeError and only catch the subclass in the first except. Then other instances of SomeError will fall through to the second except block.

Categories

Resources