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

Steven D'Aprano steve at pearwood.info
Fri Apr 13 12:43:57 EDT 2018


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


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.


> 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__().

(This assumes that assignment-expressions don't require parens. For the 
case where they are required, see below.)

That's a subtle but important difference, and it is especially awful 
because most of the time it *seems* to work. Until suddenly it doesn't.

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.


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.

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?


> Without parens -> old syntax + meaning
> with parens -> bind expr to name, because that's what the parens say.

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__()


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



-- 
Steve


More information about the Python-ideas mailing list