[Python-ideas] except expression

Chris Angelico rosuav at gmail.com
Sun Feb 16 05:04:03 CET 2014


PEP: XXX
Title: Exception-catching expressions
Version: $Revision$
Last-Modified: $Date$
Author: Chris Angelico <rosuav at gmail.com>
Status: Draft
Type: Standards Track
Content-Type: text/x-rst
Created: 15-Feb-2014
Python-Version: 3.5
Post-History: 16-Feb-2014


Abstract
========

Just as PEP 308 introduced a means of value-based conditions in an
expression, this system allows exception-based conditions to be used as part
of an expression.


Motivation
==========

A number of functions and methods have parameters which will cause them to
return a specified value instead of raising an exception.  The current system
is ad-hoc and inconsistent, and requires that each function be individually
written to have this functionality; not all support this.

* dict.get(key, default) - second positional argument in place of KeyError

* next(iter, default) - second positional argument in place of StopIteration

* list.pop() - no way to return a default

(TODO: Get more examples. I know there are some but I can't think of any now.)


Rationale
=========

The current system requires that a function author predict the need for a
default, and implement support for it. If this is not done, a full try/except
block is needed.

Note that the specific syntax is open to three metric tons of bike-shedding.

The proposal may well be rejected, but even then is not useless; it can be
maintained as a collection of failed syntaxes for expression exceptions.


Proposal
========

Just as the 'or' operator and the three part 'if-else' expression give short
circuiting methods of catching a falsy value and replacing it, this syntax
gives a short-circuiting method of catching an exception and replacing it.

This currently works::
    lst = [1, 2, None, 3]
    value = lst[2] or "No value"

The proposal adds this::
    lst = [1, 2]
    value = lst[2] except IndexError: "No value"

The exception object can be captured just as in a normal try/except block::
    # Return the next yielded or returned value from a generator
    value = next(it) except StopIteration as e: e.args[0]

This is effectively equivalent to::
    try:
        _ = next(it)
    except StopIteration as e:
        _ = e.args[0]
    value = _

This ternary operator would be between lambda and if/else in precedence.


Chaining
--------

Multiple 'except' keywords can be used, and they will all catch exceptions
raised in the original expression (only)::
    value = (expr
        except Exception1 [as e]: default1
        except Exception2 [as e]: default2
        # ... except ExceptionN [as e]: defaultN
    )

This is not the same as either parenthesized form::
    value = (("Yield: "+next(it) except StopIteration as e: "End: "+e.args[0])
except TypeError: "Error: Non-string returned or raised")
    value = (next(it) except StopIteration as e:
       (e.args[0] except IndexError: None))

The first form will catch an exception raised in either the original
expression or in the default expression; the second form will catch ONLY one
raised by the default expression. All three effects have their uses.


Alternative Proposals
=====================

Discussion on python-ideas brought up the following syntax suggestions::
    value = expr except default if Exception [as e]
    value = expr except default for Exception [as e]
    value = expr except default from Exception [as e]
    value = expr except Exception [as e] return default
    value = expr except (Exception [as e]: default)
    value = expr except Exception [as e] try default
    value = expr except Exception [as e] continue with default
    value = default except Exception [as e] else expr
    value = try expr except Exception [as e]: default
    value = expr except Exception [as e] pass default

In all cases, default is an expression which will not be evaluated unless
an exception is raised; if 'as' is used, this expression may refer to the
exception object.

It has also been suggested that a new keyword be created, rather than reusing
an existing one. Such proposals fall into the same structure as the last
form, but with a different keyword in place of 'pass'. Suggestions include
'then', 'when', and 'use'.


Open Issues
===========

finally clause
--------------

Should the except expression be able to have a finally clause? No form of the
proposal so far has included finally.


Commas between multiple except clauses
--------------------------------------
Where there are multiple except clauses, should they be separated by commas?
It may be easier for the parser, that way::
    value = (expr
        except Exception1 [as e]: default1,
        except Exception2 [as e]: default2,
        # ... except ExceptionN [as e]: defaultN,
    )
with an optional comma after the last, as per tuple rules. Downside: Omitting
the comma would be syntactically valid, and would have almost identical
semantics, but would nest the entire preceding expression in its exception
catching rig - a matching exception raised in the default clause would be
caught by the subsequent except clause. As this difference is so subtle, it
runs the risk of being a major bug magnet.


Copyright
=========

This document has been placed in the public domain.




..
   Local Variables:
   mode: indented-text
   indent-tabs-mode: nil
   sentence-end-double-space: t
   fill-column: 70
   coding: utf-8


More information about the Python-ideas mailing list