[Python-ideas] Block-Scoped Exception Handlers

cs at zip.com.au cs at zip.com.au
Wed May 4 19:05:53 EDT 2016


On 04May2016 15:58, Kyle Lahnakoski <klahnakoski at mozilla.com> wrote:
>Please excuse my nomenclature.  I hope the community can correct the
>synonyms that clarify my proposal.
>
>Problem
>-------
>
>I program defensively, and surround many of my code blocks with try
>blocks to catch expected and unexpected errors.   Those unexpected
>errors seem to dominate in my code; I never really know how many ways my
>SQL library can fail, nor am I really sure that a particular key is in a
>`dict()`.   Most of the time I can do nothing about those unexpected
>errors; I simply chain them, with some extra description about what the
>code block was attempting to do.

I also like context from close to where the exception occurred, 
while doing that catching at whatever the suitable outer layer may 
be, as normal. Let me show you what I do...

I have a module "cs.logutils":

  https://bitbucket.org/cameron_simpson/css/src/tip/lib/python/cs/logutils.py

which is also on PyPI, thus "pip install"able.

It has a context manager called "Pfx", short for "prefix". I use it like this:

  from cs.logutils import Pfx

  ...

  with Pfx(filename):
    with open(filename) as fp:
      for lineno, line in enumerate(fp, 1):
        with Pfx(lineno):
          ... do stuff here ...

If an exception occurs within a Pfx context manager, its message 
attributes get prepended with the strings of the active Pfx instances, 
joined by ": ". Then it is reriased and handled exaxtly as if there 
were no Pfxs in play. So if some ValueError occured while processing 
line 3 of the file "foo.txt" the ValueError's message would start 
"foo.txt: 3: " and proceed with the core ValueError message.

This provides me with cheap runtime context for all exceptions, 
with minimal boilerplate in my code. All the catch-and-reraise stuff 
happens in the __exit__ method of the innermost Pfx instance, if 
an exception occurs.

When no exceptions occur all it is doing is maintaining a thread 
local stack of current message prefixes.

It looks like this might address your concerns without adding things 
to Python itself. You could certainly make a case for annotating 
the exceptions with an arbitrary extra object with whatever structured 
state eg a dict); suggestions there welcome.

What do you think of this approach to your concerns?

Cheers,
Cameron Simpson <cs at zip.com.au>

>I am using 2.7, so I have made my own convention for chaining
>exceptions.  3.x chains more elegantly:
>
>    for t in todo:
>        try:
>            # do error prone stuff
>        except Exception, e:
>            raise ToDoError("oh dear!") from e
>
>The “error prone stuff” can itself have more try blocks to catch known
>failure modes, maybe deal with them.  Add some `with` blocks and a
>conditional, and the nesting gets ugly:
>
>    def process_todo(todo):
>        try:
>            with Timer("todo processing"):
>                # pre-processing
>                for t in todo:
>                    try:
>                        # do error prone stuff
>                    except Exception, e:
>                        raise TodoError("oh dear!") from e
>                # post-processing
>        except Exception, e:
>            raise OverallTodoError("Not expected") from e
>
>Not only is my code dominated by exception handling, the meaningful code
>is deeply nested.
>
>
>Solution
>--------
>
>I would like Python to have a bare `except` statement, which applies
>from that line, to the end of enclosing block (or next `except`
>statement).  Here is the same example using the new syntax:
>
>    def process_todo(todo):
>        except Exception, e:
>            raise OverallTodoError("Not expected") from e
>
>        with Timer("todo processing"):
>            # pre-processing
>            for t in todo:
>                except Exception, e:
>                    raise TodoError("oh dear!") from e
>
>                # do error prone stuff
>            # post-processing
>
>Larger code blocks do a better job of portraying he visual impact of the
>reduced indentation.  I would admit that some readability is lost
>because the error handling code precedes the happy path, but I believe
>the eye will overlook this with little practice.
>
>Multiple `except` statements are allowed.  They apply as if they were
>used in a `try` statement; matched in the order declared:
>
>    def process_todo(todo):
>        pre_processing()  # has no exception handling
>
>        except SQLException, e:  # effective until end of method
>            raise Exception("Not expected") from e
>        except Exception, e:
>            raise OverallTodoError("Oh dear!") from e
>
>        processing()
>
>A code block can have more than one `except` statement:
>
>    def process_todo(todo):
>        pre_processing()  # no exception handling
>
>        except SQLException, e:  # covers lines from here to beginning
>of next except statement
>            raise Exception("Not expected") from e
>        except Exception, e:   # catches other exception types
>            raise Exception("Oh dear!") from e
>
>        processing()  # Exceptions caught
>
>        except SQLException, e:  # covers a lines to end of method
>            raise Exception("Happens, sometimes") from e
>
>        post_processing()  # SQLException caught, but not Exception
>
>In these cases, a whole new block is effectively defined.  Here is the
>same in legit Python:
>
>    def process_todo(todo):
>        pre_processing()  # no exception handling
>
>        try:
>            processing()  # Exceptions caught
>        except SQLException, e:  # covers all lines from here to
>beginning of next except statement
>            raise Exception("Not expected") from e
>        except Exception, e:   # catches other exception types
>            raise Exception("Oh dear!") from e
>
>        try:
>            post_processing()  # SQLException caught, but not Exception
>        except SQLException, e:  # covers a lines to end of method
>            raise Exception("Happens, sometimes") from e
>
>Other Thoughts
>--------------
>
>I only propose this for replacing `try` blocks that have no `else` or
>`finally` clause.  I am not limiting my proposal to exception chaining;
>Anything allowed in `except` clause would be allowed.
>
>I could propose adding `except` clauses to each of the major statement
>types (def, for, if, with, etc…).  which would make the first example
>look like:
>
>    def process_todo(todo):
>        with Timer("todo processing"):
>            # pre-processing
>            for t in todo:
>                # do error prone stuff
>            except Exception, e:
>                raise TodoError("oh dear!") from e
>
>            # post-processing
>    except Exception, e:
>        raise OverallTodoError("Not expected") from e
>
>But, I am suspicious this is more complicated than it looks to
>implement, and the `except` statement does seem visually detached from
>the block it applies to.
>
>
>Thank you for your consideration!
>
>
>_______________________________________________
>Python-ideas mailing list
>Python-ideas at python.org
>https://mail.python.org/mailman/listinfo/python-ideas
>Code of Conduct: http://python.org/psf/codeofconduct/

-- 


More information about the Python-ideas mailing list