[Python-ideas] except expression

Chris Angelico rosuav at gmail.com
Sun Feb 16 14:01:19 CET 2014


On Sun, Feb 16, 2014 at 11:01 PM, Jan Kaliszewski <zuo at chopin.edu.pl> wrote:
> In terms of my proposal it would clearly be just:
>
>     expr except (Exception1: default1) except (Exception2: default2)
>
> Of course grouping parens could be added without changing semantics:
>
>     (expr except (Exception1: default1)) except (Exception2: default2)
>
> Also, see below...

Actually that _does_ change semantics, but in an extremely subtle way.
In both versions, expr raising Exception2 will result in default2; but
the first version is effectively this:

try:
    _ = expr
except Exception1:
    _ = default1
except Exception2:
    _ = default2
value_of_expression = _

If the evaluation of default1 raises Exception2, then this form will
propagate that exception up. The second form is actually like this:

try:
    try:
        _ = expr
    except Exception1:
        _ = default1
except Exception2:
    _ = default2
value_of_expression = _

In this, the evaluation of default1 is inside the outer try block, so
if it raises Exception2, the expression result will be default2. It's
an extremely subtle difference (since, in most normal cases, the
default expressions will be very simple), but it is a difference.

Also, the interpreter's forced to construct and process two try/except
blocks, for what that's worth. I expect this won't ever be significant
in a real-world case, but there is a difference:

>>> timeit.repeat("try: 1/0\nexcept NameError: pass\nexcept ZeroDivisionError: pass")
[2.3123623253793433, 2.240881173445228, 2.2435943674405543]
>>> timeit.repeat("try:\n try: 1/0\n except NameError: pass\nexcept ZeroDivisionError: pass")
[2.9560086547312565, 2.958433264562956, 2.9855417378465674]

That's of the order of five microseconds per iteration, if I'm reading
my figures correctly, so that's not exactly significant... but I still
see no reason to go to the extra work unnecessarily :)

> possibly extendable to:
>
>     # with 'as' clause'
>     some_io() except (OSError as exc: exc.errno)

Definitely want to keep the 'as' clause in there, although it does
raise some issues of its own as regards name binding.

(responding out of order as your code example nicely illustrates your
next point)
> Before I posted the proposal I did think about the "things[i] (except
> ..." variant also but I don't like that the opening parenthesis
> character suggest to a human reader that it is a call...  On the other
> hand, when the parenthesis is *after* the 'except' keyword it is clear
> for human readers that it is a dedicated syntax having nothing to do
> with a call.

I don't see it as particularly significant either way. Opening parens
and immediately having a keyword like "except" is strongly indicative
of something different going on; same goes for having that keyword
just _before_ the parens.

> ...and/or:
>
>     # with multiple exception catches
>     some_io() except (FileNotFoundError: 42,
>                       OSError as exc: exc.errno)
>
> Also, for a multiple-exception-catches-variant I don't like repeating
> the 'except' keyword for each catch, as well as the ambiguity whether
> the consecutive 'except...' concerns only the initial expression or
> also the preceding 'except...').  In my proposal there is no such
> ambiguity.

There's an ambiguity in this version, though. After the 'except'
keyword, you can have a tuple of exceptions:

try:
    1/x
except (NameError, ZeroDivisionError):
    print("Can't divide by nothing!")

If the previous value is followed by a comma and the next thing could
be a tuple, the parsing is going to get a bit hairy. Either the
previous value or the exception_list could be a tuple. I'd rather
repeat the word 'except'.

> I also believe that making the parens *obligatory* is a good thing

I'm happy with them being optional in cases where it's unambiguous,
but I'd be okay with mandating them to disambiguate. After all, if the
language says they're optional, style guides have the option of
mandating; but if the language mandates, the freedom is gone.

> More complex (though probably not very realistic) example:
>
>     msg = (cache[k] except (
>                LookupError: backend.read() except (
>                    OSError: 'resource not available'))
>            if check_permission(k)
>            else 'access not allowed'
>           ) except (Exception: 'internal error occurred')

Fairly unrealistic, as the backend.read() call won't get put into the
cache (and you can't put the final msg into the cache, as 'resource
not available' sounds temporary and thus non-cached), not to mention
that swallowing Exception to just return a constant string seems like
a really REALLY bad idea! :) But this highlights another big issue:
How should a complex except expression be laid out? With the statement
form, it's pretty clear: first you do the line(s) that you're trying,
then you have your except clauses, no more than one on any given line,
and then you have your else, and your finally, if you have either.
Everything's on separate lines. What about here? If it all fits on one
line, great! But it often won't (the name "ZeroDivisionError" is over
a fifth of your typical line width, all on its own), and then where do
you break it? Not technically part of the proposal, but it's going to
be an issue.

ChrisA


More information about the Python-ideas mailing list