PEP 758: Allow `except` and `except*` expressions without parentheses

This change would bring the syntax more in line with other comma-separated lists in Python, such as function arguments, generator expressions inside of a function call, and tuple literals, where parentheses are optional.

When are parentheses optional around lists of function arguments?

Also, as others have mentioned, import , with and case would seem like more relevant comparisons than any of these examples.

3 Likes

In a lambda, but I don’t think that’s what they meant…so I also think that’s confusing.

edit: actually they’re forbidden in a lambda, so that’s definitely not it :joy:

I must admit I have accidentally written the proposed form before, except A, B, C:.

The semantics of as keyword are perhaps debatable, somehow I naively think that as keyword binds tighter than , comma, making except A, E as e: as a bit of a WAT moment.

The precedent would be with open("a"), open("b") as f: that folks may be used to.

Then again, maybe it’s just me, and maybe folks will get used to it.

4 Likes

Please vote in this friendly non-binding poll about your preference regarding parentheses when using the “as” keyword:

  • I want to always require parentheses when using the “as” keyword in except blocks
  • I want to drop the parentheses requirement even when using the “as” keyword in except blocks
0 voters
2 Likes

My position is that the syntax itself should support not using parentheses, for consistency, but style guides (including PEP 8) and linters should strongly encourage using them, and I would also like the PEP to talk about this potential point of confusion.

11 Likes

While I initially voted in favour of “permissive compiler, restrictive linting” where as is concerned, there’s a concrete “least work for most benefit” gain from continuing to require parentheses with the as clause: any work associated with updating style guides to specify that guideline and linters to enforce it won’t need to happen, since compilers will continue to handle it on everyone’s behalf.

That’s actually a pretty decent “practicality beats purity” argument in favour of enforcing this particular linting guideline at the compiler level - the parentheses are really only annoying when you’re not using as with multiple exception types, so being required to add them when you are is just forcing you to do something you should be doing for readability reasons anyway.

“It should be a linting rule not a syntax restriction”, while true, doesn’t feel like a particularly compelling reason to inflict a bunch of avoidable work on people, so I changed my vote in the informal poll accordingly (even if it does make the language grammar a little messier than it would otherwise be).

14 Likes

We will surely include this in the document :+1:

1 Like
>>> from os import getuid, getenvb as getenv_bytes
>>> with open('/dev/null', 'rb') as empty_f, open('/dev/zero', 'rb') as full_f:
...   empty_f.read(), full_f.read(1)
...
(b'', b'\x00')

Making except ValueError, CustomError as exc: not require parenthesis around the list of Error classes makes the binding behavior of as inconsistent in different areas of the language so that it requires more context to understand.

I don’t think it will really be confusing, but it is an inconsistency nonetheless. I don’t think anyone enjoys typing those ()s. When editing code to add an additional exception class to an except clause where there is only one, it requires more keyboard gymnastics or editor tricks when they are required. Lightening up the syntax requirement would alleviate that.

(all of this may have been stated already, I haven’t read the whole thread. This is just me justifying my thought process outloud)

4 Likes

How common are except clauses with multiple types? My intuition is that they are unusual enough for ergonomics not to matter that much.

3 Likes

One thing I realized is we aren’t just putting “parentheses around around the list of Error classes”, you’re making a tuple. And you aren’t binding what that tuple is but some relation of what that except clause does with that tuple.

Another option is to only require parentheses when as is involved, which makes the parentheses necessary to disambiguate and thus not an odd use of them.

6 Likes

Running against main using rg --stats <regex>:

  • “^\s*except( |:)”: 6,430
  • “^\s*except:”: 399
  • “^\s*except (”: 488
  • “^\s*except [^(]”: 5,543
1 Like

A bit of a side tracking, and looking into the future with PEP 760 - The main python branch contains 399 bare except clauses? That is kind of worrying, I hope they aren’t in any relevant libraries…

Bare except with an unconditional raise is actually OK (and the stdlib has quite a few transactional APIs with rollback semantics).

8 Likes

Yes, and that is often, but not always the case. Quick spot check with github search makes me guess 1/8 being problematic in abstract (i.e. no raise), and maybe another 1/8 of those are in potentially public facing parts, although in unlikely code paths if I am seeing it correctly. But not something to worry about now.

10 posts were merged into an existing topic: PEP 760 – No More Bare Excepts

Hah, I guess that is actually true. I never gave the underlying mechanics much thought. Indeed you can bind a tuple to a name and use it in an except clause for the same effect. I don’t know that I’ve ever seen code written that way but it is supported and I’m sure some code uses it. I presume the original thinking if not implementation was that the class or tuple in an except clause is basically same as what you could pass to isinstance().

>>> t = (ValueError, RuntimeError)
>>> try:
...   raise RuntimeError("knigit")
... except t as e:
...   print(e)
...
knigit
>>>

I do like the “only if needed to disambiguate” idea as an intermediate option if not requiring parens here ever isn’t acceptable.

5 Likes

It’s quite useful to write functions like unittest’s assertRaises (though assertRaises itself is too complex for a simple try/except).

Almost, but there’s a weird inconsistency: except doesn’t accept nested tuples. You can do:

>>> errors = AttributeError, ArithmeticError
>>> try:
...     1/0
... except errors as err:
...     print(err)
... 
division by zero

but not:

>>> try:
...     1/0
... except (errors, ValueError) as err:
...     print(err)
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
TypeError: catching classes that do not inherit from BaseException is not allowed

Nested tuples do work with isinstance/issubclass:

>>> isinstance(ArithmeticError(), (errors, ValueError))
True

FWIW, this worked in 2.7, before 3.0 disallowed catching non-BaseException objects. I wonder if it was an intentional change.

Anyway, nowadays one can easily make a flat tuple with (*errors, ValueError), so it’s not a big deal in practice.

1 Like

Petr,

I may be missing something in your argument.

Why should nested anything be accepted in a context where something like an iterable producing flat members is almost always expected?

There are other scenarios like you mention, such as things with multiple inheritance, that are not necessarily flat by nature. Or, a tree structure may require flattening in a depth first or breadth first or other manner. In such cases, if a flat version is needed, and it is obvious which is the unambiguous method for flattening it, go for it.

Certainly if efficiency is not a consideration and some very broad generality is valued, you could flatten such a nested tuple before further evaluation or allow much more complex processing such as removing duplicates you can generate.

But is there much value when, instead, the occasional user does it manually so that the more common case is simpler and can be handled efficiently?

Avi

Petr wasn’t making an argument, but pointing out a fact: nested tuples are supported for isinstance(), but not for except:.

4 Likes

Brett,

This is a side question about how tuples are handled in python, perhaps in general.

My previous understanding is that what makes a tuple is the comma, not the parentheses.

So, unless the handling of tuples in the except case is different, is there a difference between evaluating cases like these:

a, _, c = 1, 2, 3
(a, _, c) = (1, 2, 3)

There can be other variations but the question I have is whether you can say one of many similar methods will create a (temporary) tuple while others don’t?

The case you are discussing can perhaps be easy to investigate by looking at the byte code produced in these two variant of
... except alpha, beta ...
where the ... just means there will be more code on lines above and below, versus:

... except (alpha, beta) ...

Python does overload usage of parentheses in various ways but in many contexts, such as using parentheses for grouping, the code produced if you are not changing precedence rules would probably be the same but the parentheses used would make it easier for human readers of the code to be clear on what it will do.

I am not expressing an opinion here on whether the parentheses here should be optional, but just asking if this observation is correct, or if a tuple is automatically created just because of the comma.

Avi