[Python-Dev] PEP 463: Exception-catching expressions

Nick Coghlan ncoghlan at gmail.com
Fri Feb 21 12:35:20 CET 2014


On 21 February 2014 13:15, Chris Angelico <rosuav at gmail.com> wrote:
> PEP: 463
> 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, 21-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.

Great work on this Chris - this is one of the best researched and
justified Python syntax proposals I've seen :)

> Open Issues
> ===========
>
> Parentheses around the entire expression
> ----------------------------------------
>
> Generator expressions require parentheses, unless they would be
> strictly redundant.  Ambiguities with except expressions could be
> resolved in the same way, forcing nested except-in-except trees to be
> correctly parenthesized and requiring that the outer expression be
> clearly delineated.  `Steven D'Aprano elaborates on the issue.`__
>
> __ https://mail.python.org/pipermail/python-ideas/2014-February/025647.html

I'd like to make the case that the PEP should adopt this as its
default position. My rationale is mainly that if we start by requiring
the parentheses, it's pretty straightforward to take that requirement
away in specific cases later, as well as making it easier to introduce
multiple except clauses if that ever seems necessary.

However, if we start without the required parentheses, that's it - we
can't introduce a requirement for parentheses later if we decide the
bare form is too confusing in too many contexts, and there's plenty of
potential for such confusion.

In addition to the odd interactions with other uses of the colon as a
marker in the syntax, including suite headers, lambdas and function
annotations, omitting the parentheses makes it harder to decide which
behaviour was intended in ambiguous cases, while the explicit
parentheses would force the writer to be clear which one they meant.
Consider:

    x = get_value() except NotFound: foo is not None

There are two plausible interpretations of that:

    x = (get_value() except NotFound: foo) is not None
    x = get_value() except NotFound: (foo is not None)

With the proposed precedence in the PEP (which I agree with), the
latter is the correct interpretation, but that's not at all obvious to
the reader - they would have to "just know" that's the way it works.
By contrast, if the parentheses are required, then the spelling would
have to be one of the following to be legal:

    x = (get_value() except NotFound: foo is not None)
    x = (get_value() except NotFound: foo) is not None

Which means the ":" and the closing ")" nicely bracket the fallback
value in both cases and make the author's intent relatively clear.

The required parentheses also help in the cases where there is a
nearby colon with a different meaning:

   if check() except Exception: False:
        ...
   if (check() except Exception: False):
        ...

   lambda x: calculate(x) except Exception: None
   lambda x: (calculate(x) except Exception: None)

   def f(a: "OS dependent" = os_defaults[os.name] except KeyError: None): pass
   def f(a: "OS dependent" = (os_defaults[os.name] except KeyError: None)): pass

Rather than making people consider "do I need the parentheses in this
case or not?", adopting the genexp rule makes it simple: yes, you need
them, because the compiler will complain if you leave them out.

> Retrieving a message from either a cache or the internet, with auth
> check::
>
>     logging.info("Message shown to user: %s",((cache[k]
>         except LookupError:
>             (backend.read(k) except OSError: 'Resource not available')
>         )
>         if check_permission(k) else 'Access denied'
>     ) except BaseException: "This is like a bare except clause")

I don't think taking it all the way to one expression shows the new
construct in the best light. Keeping this as multiple statements
assigning to a temporary variable improves the readability quite a
bit:

    if not check_permission(k):
        msg = 'Access denied'
    else:
        msg = (cache[k] except LookupError: None)
        if msg is None:
            msg = (backend.read(k) except OSError: 'Resource not available')

    logging.info("Message shown to user: %s", msg)

I would also move the "bare except clause" equivalent out to a
separate example. Remember, you're trying to convince people to *like*
the PEP, not scare them away with the consequences of what happens
when people try to jam too much application logic into a single
statement. While we're admittedly giving people another tool to help
them win obfuscated Python contests, we don't have to *encourage* them
:)

>     try:
>         if check_permission(k):
>             try:
>                 _ = cache[k]
>             except LookupError:
>                 try:
>                     _ = backend.read(k)
>                 except OSError:
>                     _ = 'Resource not available'
>         else:
>             _ = 'Access denied'
>     except BaseException:
>         _ = "This is like a bare except clause"
>     logging.info("Message shown to user: %s", _)

A real variable name like "msg" would also be appropriate in the
expanded form of this particular example.


> Deferred sub-proposals
> ======================
>
> Capturing the exception object
> ------------------------------
>
> An examination of the Python standard library shows that, while the use
> of 'as' is fairly common (occurring in roughly one except clause in five),
> it is extremely *uncommon* in the cases which could logically be converted
> into the expression form.  Its few uses can simply be left unchanged.
> Consequently, in the interests of simplicity, the 'as' clause is not
> included in this proposal.  A subsequent Python version can add this without
> breaking any existing code, as 'as' is already a keyword.

We can't defer this one - if we don't implement it now, we should
reject it as a future addition. The reason we can't defer it is
subtle, but relatively easy to demonstrate with a list comprehension:

    >>> i = 1
    >>> [i for __ in range(1)]
    [1]
    >>> class C:
    ...     j = 2
    ...     [i for __ in range(1)]
    ...     [j for __ in range(1)]
    ...
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 4, in C
      File "<stdin>", line 4, in <listcomp>
    NameError: global name 'j' is not defined

The problem is that "<listcomp>" scope that is visible in the
traceback - because it's a true closure, it's treated the same as any
other function defined inside a class body, and the subexpressions
can't see the class level variables.

An except expression that allowed capturing of the exception would
need to behave the same way in order to be consistent, but if we don't
allow capturing, then we can dispense with the closure entirely.
However, if we do that, we can't decide to add capturing later,
because that would mean adding the closure, which would be potentially
backwards incompatible with usage at class scopes. And if we only add
the closure if the exception is captured, then the behaviour of the
other subexpressions will depend on whether the exception is captured
or not, and that's just messy.

So I think it makes more sense to reject this subproposal outright -
it makes the change more complex and make the handling of the common
case worse, for the sake of something that will almost never be an
appropriate thing to do.

Cheers,
Nick.

-- 
Nick Coghlan   |   ncoghlan at gmail.com   |   Brisbane, Australia


More information about the Python-Dev mailing list