[Python-ideas] Spelling of Assignment Expressions PEP 572 (was post #4)

Chris Angelico rosuav at gmail.com
Fri Apr 13 16:31:31 EDT 2018


On Sat, Apr 14, 2018 at 2:43 AM, Steven D'Aprano <steve at pearwood.info> wrote:
> On Fri, Apr 13, 2018 at 05:04:00PM +0200, Jacco van Dorp wrote:
>
>> > I'm saying, don't even try to distinguish between the forms with or
>> > without parens. If we add parens:
>> >
>> >     with (expr as name):
>> >
>> > it may or may not be allowed some time in the future (since it isn't
>> > allowed now, but there are many requests for it) but if it is allowed,
>> > it will still mean a context manager and not assignment expression.
>> >
>> > (In case it isn't obvious, I'm saying that we need not *require* parens
>> > for this feature, at least not if the only reason for doing so is to
>> > make the with/except case unambiguous.)
>> >
>>
>> So if I read this correctly, you're making an argument to ignore parens ?
>
> You can always add unneeded parentheses for grouping that have no
> effect:
>
> py> value = ((((( 1 )) + (((1))))))
> py> value
> 2

That is true ONLY in an expression, not in all forms of syntax. Any
place where you can have expression, you can have (expression). You
cannot, however, add parentheses around non-expression units:

(x = 1) # SyntaxError
(for x in range(2)): # SyntaxError

assert (False, "ham") # Won't raise
assert False, "spam" # Will raise

There is a special case for import statements:

from sys import (modules)

but for everything else, you can only parenthesize expressions (aka
"slabs of syntax that evaluate, eventually, to a single object").

> One should never be penalised by the interpreter for unnecessary
> parentheses. If they do nothing, it shouldn't be an error. Do you really
> want an error just because you can't remember operator precendence?
>
>     flag = (spam is None) or (len(spam) == 0)
>
> The parens are unnecessary, but I still want to write them. That
> shouldn't be an error.

The RHS of an assignment is an expression. But you can't move that
first '(' any further to the left.

>> If I'd type with (expr as name) as othername:, I'd expect the original value
>> of expr in my name and the context manager's __enter__ return value in
>> othername. I don't really see any ambiguity in that case.
>
> That case would be okay. But the ambiguity comes from this case:
>
>     with expr as name:
>
>
> That could mean either of:
>
> 1. Assignment-as-expression, in which case <name> gets set to the value
> of <expression> and __enter__ is never called;
>
> 2. With-statement context manager, in which case <name> gets set to the
> value of <expression>.__enter__().

Since it's pre-existing syntax, the only valid interpretation is #2.
But if parenthesized, both meanings are plausible, and #1 is far more
sane (since #2 would demand special handling in the grammar).

> The problem is that the most common context manager objects return
> themselves from __enter__, so it doesn't matter whether <name> is set to
> <expression> or <expression>.__enter__(), the result will be the same.
>
> But some context managers don't work like that, and so your code will
> have a non-obvious bug just waiting to bite.

Right.

> How about if we require parentheses? That will mean that we treat these
> two statements as different:
>
>     with expr as name:  # 1
>
>     with (expr as name):  # 2
>
> Case #1 is of course the current syntax, and it is fine as it is.
>
> It is the second case that has problems. Suppose the expression is
> really long, as so you innocently intend to write:
>
>     with (really
>           long
>           expression) as name:
>
> but you accidently put the closing parenthesis in the wrong place:
>
>     with (really
>           long
>           expression as name):
>
>
> as is easy enough to do. And now you have a suble bug: name will no
> longer be set to the result of calling __enter__ as you expect.

Sure. And that's a good reason to straight-up *forbid*
expression-as-name in a 'with' statement.

> It is generally a bad idea to have the presence or absense of
> parentheses *alone* to make a semantic difference. With very few
> exceptions, it leads to problems. For example, if you remember except
> clauses back in the early versions of Python 2, or 1.5, you will
> remember the problems caused by treating:
>
>     except NameError, ValueError:
>
>     except (NameError, ValueError):
>
> as every-so-subtly different. If you don't remember Python that long
> ago, would you like to guess the difference?

The 'as' syntax was around when I started using Python seriously, but
the comma was still legal right up until 2.7, so I was aware of it.
And yes, the parens here make a drastic difference, and that's bad.

> It isn't the parentheses that cause the binding. It is the "as". So if
> you move a perfectly innocent assignment-expression into a with
> statement, the result will depend on whether or not it came with
> parentheses:
>
>
>     # these do the same thing
>     while expression as name:
>     while (expression as name):
>
>     # so do these
>     result = [1, expression as name, name + 2]
>     result = [1, (expression as name), name + 2]
>
>     # but these are subtly different and will be a trap for the unwary
>     with expression as name:  # name is set to __enter__()
>     with (expression as name):  # name is not set to __enter__()

And that's a good reason to reject the last one with a SyntaxError,
but that creates an odd discrepancy where something that makes perfect
logical sense is rejected.

> Of course, we could insist that parens are ALWAYS required around
> assignment-expressions, but that will be annoying.

Such a mandate would, IMO, come under the heading of "foolish
consistency". Unless, of course, I'm deliberately trying to get this
proposal rejected, in which case I'd add the parens to make it look
uglier :)

ChrisA


More information about the Python-ideas mailing list