Re: [Python-ideas] Consistent programming error handling idiom

Oops, good call. That was a bad example. Maybe this one is a bit better:
try: assert no_bugs() except AssertionError: # bugs are okay pass
Not sure I understand this example. It's obviously a toy, because you'd never put an 'assert' just to catch and discard the exception - the net result is that you call no_bugs() if you're in debug mode and don't if you're not, which is more cleanly spelled "if __debug__: no_bugs()".
The example is removed from its original context. The point was to show that there is no reason to locally catch AssertionError (contrast with AttributeError, ValueError, TypeError, etc.). AssertionError is *always* indicative of a bug (unlike AttributeError which only may be indicative of a bug).
This is what I'd call a boundary location. You have "outer" code and "inner" code. Any uncaught exception in the inner code should get logged rather than aborting the outer code.
I'm very familiar with this pattern in Python and I've used it myself countless times. Unfortunately I've seen instances where it can lead to disastrous behavior, I think we all have. It seems to have limited usefulness in production and more usefulness in development. The point of my original email was precisely to put the legitimacy and usefulness of code like that into question.
When does this lead to disaster? Is it because someone creates a boundary that shouldn't exist, or because the code maintains global state in bad ways? The latter is a problem even without exceptions; imagine a programming error that doesn't cause an exception, but just omits some crucial "un-modify global state" call. You have no way of detecting that in the outer code, and your state is messed up.
This leads to disaster in the case of buggy code. This leads to a greater disaster when the boundary allows the code to continue running. It's not a black and white thing, there is no 100% way to detect for buggy code but my argument is that you shouldn't ignore an assert when you're lucky enough to get one.
As to resetting stuff: I wouldn't bother; your functions should already not mess with global state. The only potential mess you should consider dealing with is a database rollback; and actually, my personal recommendation is to do that with a context manager inside the inner code, rather than a reset in the exception handler in the outer code.
So I agree this pattern works if you assume all code is exception-safe (context managers will clean up intermediate state) and there are no programming errors. There are lots of things that good code should do but as we all well know good code doesn't always do those things. My point is that except-log loops are dangerous and careless in the face of programming errors.
I don't think the except-log loop is the problem here. The problem is the code that can go in part way, come out again, and leave itself in a mess.
I'm not saying except-log is to blame for buggy code. I'm saying that when an exception occurs, there's no way to tell whether it was caused by an internal bug or an external error. Except-log should normally be limited to exceptions caused by external errors in production code.
Programming errors are unavoidable. In a large system they are a daily fact of life. When there is a bug in production it's very dangerous to re-enter a code block that has demonstrated itself to be buggy, else you risk corrupting data. For example:
def random_code(state): assert is_valid(state.data)
def user_code_a(state): state.data = "bad data" # the following throws random_code(state)
def user_code_b(state): state.db.write(state.data)
def main_loop(): state = State() loop = [user_code_a, user_code_b] for fn in loop: try: fn() except Exception: log_exception()
This code allows user_code_b() to execute and corrupt data even though random_code() was lucky enough to be called and detect bad state early on.
Can you give a non-toy example that has this kind of mutable state at top level? I suspect it's bad design. If it's truly necessary, use a context manager to guarantee the reset:
def user_code_a(state): with state.set_data("bad data"): random_code(state)
Yes it may be bad design or the code may have bugs but that's precisely the point. By the time the exception hits your catch-all, there's no universal way of determining whether or not the exception was due to an internal bug (or bad design or whatever) or an external error.
You may say the fix is to assert correct data before writing to the database and, yes, that would fix the problem for future executions in this instance. That's not the point, the point is that incorrect buggy code is running in production today and it's imperative to have multiple safeguards to limit its damage. For example, you wouldn't have an except-log loop in an airplane control system.
Actually, yes I would. The alternative that you're suggesting is to have any error immediately shut down the whole system. Is that really better? To have the entire control system disabled?
The alternative I'm suggesting is to reset that flight computer and switch to the backup one (potentially written by a different team).
Sometimes an error is just an error but sometimes an error signifies the running system itself is in a bad state. It would be nice to distinguish between the two in a consistent way across all Python event loops. Halting on any escaped exception is inconvenient, but continuing after any escape exception is dangerous.
There's no way for Python to be able to fix this for you. The tools exist - most notably context managers - so the solution is to use them.
Context managers don't allow me to determine whether an exception is caused by an internal bug or external error, so it's not a solution to this problem. I want to live in a world where I can do this:
while cbs: cb = cbs.pop() try: cb() except Exception as e: logging.exception("In main loop") if is_a_bug(e): raise SystemExit() from e
Python may be able to do something here. One possible thing is a new exception hierarchy, there may be other solutions. This may be sufficient but I doubt it:
def is_a_bug(e): return isinstance(e, AssertionError)
The reason I am polling python-ideas is that I can't be the only one who has ever encountered this deficiency in Python.
Rian

On Sat, Apr 9, 2016 at 4:24 AM, rian@thelig.ht wrote:
Python may be able to do something here. One possible thing is a new exception hierarchy, there may be other solutions. This may be sufficient but I doubt it:
def is_a_bug(e): return isinstance(e, AssertionError)
The reason I am polling python-ideas is that I can't be the only one who has ever encountered this deficiency in Python.
How about this:
def is_a_bug(e): return True
ANY uncaught exception is a bug. Any caught exception is not a bug.
ChrisA

On Sat, Apr 09, 2016 at 04:28:47AM +1000, Chris Angelico wrote:
ANY uncaught exception is a bug.
What, even SystemExit and KeyboardInterrupt?
Any caught exception is not a bug.
Even:
try: ... except: # Look, bug free programming! pass
I am sure that any attempt to make universal rules about what is or isn't a bug will be doomed to failure. Remember that people can write code like this:
# Writing a BASIC interpreter in Python. if not isinstance(command[0], int): raise SyntaxError('expected line number')
So, no, SyntaxError does not necessarily mean a bug in your code. *ALL* exceptions have to be understood in context, you can't just make a sweeping generalisation that exception A is always a bug, exception B is always recoverable. It depends on the context.

On Sat, Apr 9, 2016 at 5:01 AM, Steven D'Aprano steve@pearwood.info wrote:
On Sat, Apr 09, 2016 at 04:28:47AM +1000, Chris Angelico wrote:
ANY uncaught exception is a bug.
What, even SystemExit and KeyboardInterrupt?
In terms of a boundary location? Yes, probably. If you're writing a web server that allows web applications to be plugged into it, you probably want to let the console halt processing of one request, then go back and handle another. Although it is contextual; you might choose the other way, in which case it's "any uncaught subclass of Exception is a bug", or "anything other than KeyboardInterrupt is a bug", or some other definition. I'm on the fence about SystemExit; in any context where there's this kind of boundary, sys.exit() simply wouldn't be used. So it could viably be called a bug, or it could validly be used to terminate the entire server.
Any caught exception is not a bug.
Even:
try: ... except: # Look, bug free programming! pass
As far as the boundary location's concerned, yes. The inner code can be as ridiculous as it likes, but the boundary will never do the "log and resume" if all exceptions are caught. (It won't even get an opportunity to.)
I am sure that any attempt to make universal rules about what is or isn't a bug will be doomed to failure. Remember that people can write code like this:
# Writing a BASIC interpreter in Python. if not isinstance(command[0], int): raise SyntaxError('expected line number')
So, no, SyntaxError does not necessarily mean a bug in your code. *ALL* exceptions have to be understood in context, you can't just make a sweeping generalisation that exception A is always a bug, exception B is always recoverable. It depends on the context.
If one escapes to a boundary, then yes it does. That's the context we're talking about here.
ChrisA

On Fri, Apr 8, 2016 at 11:24 AM, rian@thelig.ht wrote:
AssertionError is *always* indicative of a bug
I don't think there is any way to know that -- it all depends on how it's used. For my part, Assertions are only for testing -- and are actually turned off in debug mode.
And I think we can say the same thing about ALL Exceptions --even a syntax error may not be a "stop everything" error in a system that runs user-written scripts.
I agree with Chris A's point:
Any unhandled Exception is a bug. Simple as that.
Any other interpretation would be a style issue, decided for your group/application.
And if you are going to go there, I would do:
if not_a_bug() instead.
Consistent with the principle of good Python code -- only handle the Exceptions you know to handle.
-CHB

On Apr 8, 2016, at 12:02 PM, Chris Barker chris.barker@noaa.gov wrote: I agree with Chris A's point:
Any unhandled Exception is a bug. Simple as that.
I'm happy with that interpretation. If that was codified in a style document accessible to newbies I think that would help achieve a more consistent approach to exceptions.
Yet something dark and hideous inside me tells me except-log loops will continue to be pervasive and bugs will continue to be ignored in a large number of Python programs.

On 04/08/2016 12:47 PM, Rian Hunter wrote:
On Apr 8, 2016, at 12:02 PM, Chris Barker wrote:
I agree with Chris A's point:
Any unhandled Exception is a bug. Simple as that.
I'm happy with that interpretation. If that was codified in a style document accessible to newbies I think that would help achieve a more consistent approach to exceptions.
Yet something dark and hideous inside me tells me except-log loops will continue to be pervasive and bugs will continue to be ignored in a large number of Python programs.
Sadly, the language cannot force someone to investigate problems instead of ignoring them. :(
-- ~Ethan~

On Fri, Apr 8, 2016 at 12:47 PM, Rian Hunter rian@thelig.ht wrote:
On Apr 8, 2016, at 12:02 PM, Chris Barker chris.barker@noaa.gov wrote: I agree with Chris A's point:
Any unhandled Exception is a bug. Simple as that.
I'm happy with that interpretation. If that was codified in a style document accessible to newbies I think that would help achieve a more consistent approach to exceptions.
it already is in advice all over the place: "don't use bare except"
Doesn't mean folks don't do it anyway....
-CHB

On Apr 8, 2016, at 1:47 PM, Chris Barker chris.barker@noaa.gov wrote:
On Fri, Apr 8, 2016 at 12:47 PM, Rian Hunter rian@thelig.ht wrote:
On Apr 8, 2016, at 12:02 PM, Chris Barker chris.barker@noaa.gov wrote: I agree with Chris A's point:
Any unhandled Exception is a bug. Simple as that.
I'm happy with that interpretation. If that was codified in a style document accessible to newbies I think that would help achieve a more consistent approach to exceptions.
it already is in advice all over the place: "don't use bare except"
Doesn't mean folks don't do it anyway....
I think bare except is different from "except Exception" which is common and not discouraged. "except Exception" still masks programming errors.

On 04/08/2016 01:50 PM, Rian Hunter wrote:
I think bare except is different from "except Exception" which is common and not discouraged. "except Exception" still masks programming errors.
And comes with the advice to "log it, research it".
-- ~Ethan~

On 08/04/2016 21:50, Rian Hunter wrote:
I think bare except is different from "except Exception" which is common and not discouraged. "except Exception" still masks programming errors.
"except Exception" is a programming error as far as I'm concerned, but I'm not expecting my own code to keep running 24/7/365. Horses for courses.

On Fri, Apr 08, 2016 at 11:24:15AM -0700, rian@thelig.ht wrote:
I want to live in a world where I can do this:
while cbs: cb = cbs.pop() try: cb() except Exception as e: logging.exception("In main loop") if is_a_bug(e): raise SystemExit() from e
And I want a pony :-)
So long as Python allows people to write code like this:
# A deliberately silly example if len(seq) == 0: raise ImportError("assertion fails")
you cannot expect to automatically know what an exception means without any context of where it came from and why it happened. The above example is silly, but it doesn't take much effort to come up with more serious ones:
- an interpreter written in Python may raise SyntaxError, which is not a bug in the interpreter;
- a test framework may raise AssertionError for a failed test, which is not a bug in the framework;
- a function may raise MemoryError if the call *would* run out of memory, but without actually running out of memory; consequently it is not a fatal error, while another function may raise the same MemoryError because it actually did fatally run out of memory.
Effectively, you want the compiler to Do What I Mean when it comes to exceptions. DWIM may, occasionally, be a good idea in applications, but I maintain it is never a good idea in a programming language.
http://www.catb.org/jargon/html/D/DWIM.html
I'm afraid that there's no hope for it: you're going to have to actually understand where an exception came from, and why it happened, before deciding whether or not it can be recovered from. The interpreter can't do that for you.
participants (7)
-
Chris Angelico
-
Chris Barker
-
Ethan Furman
-
Mark Lawrence
-
Rian Hunter
-
rian@thelig.ht
-
Steven D'Aprano