PEP 572: Assignment Expressions (post #4)

Wholesale changes since the previous version. Statement-local name bindings have been dropped (I'm still keeping the idea in the back of my head; this PEP wasn't the first time I'd raised the concept), and we're now focusing primarily on assignment expressions, but also with consequent changes to comprehensions. Sorry for the lengthy delays; getting a reference implementation going took me longer than I expected or intended. ChrisA PEP: 572 Title: Assignment Expressions Author: Chris Angelico <rosuav@gmail.com> Status: Draft Type: Standards Track Content-Type: text/x-rst Created: 28-Feb-2018 Python-Version: 3.8 Post-History: 28-Feb-2018, 02-Mar-2018, 23-Mar-2018, 04-Apr-2018 Abstract ======== This is a proposal for creating a way to assign to names within an expression. Additionally, the precise scope of comprehensions is adjusted, to maintain consistency and follow expectations. Rationale ========= Naming the result of an expression is an important part of programming, allowing a descriptive name to be used in place of a longer expression, and permitting reuse. Currently, this feature is available only in statement form, making it unavailable in list comprehensions and other expression contexts. Merely introducing a way to assign as an expression would create bizarre edge cases around comprehensions, though, and to avoid the worst of the confusions, we change the definition of comprehensions, causing some edge cases to be interpreted differently, but maintaining the existing behaviour in the majority of situations. Syntax and semantics ==================== In any context where arbitrary Python expressions can be used, a **named expression** can appear. This can be parenthesized for clarity, and is of the form ``(target := expr)`` where ``expr`` is any valid Python expression, and ``target`` is any valid assignment target. The value of such a named expression is the same as the incorporated expression, with the additional side-effect that the target is assigned that value. # Similar to the boolean 'or' but checking for None specifically x = "default" if (eggs := spam().ham) is None else eggs # Even complex expressions can be built up piece by piece y = ((eggs := spam()), (cheese := eggs.method()), cheese[eggs]) Differences from regular assignment statements ---------------------------------------------- An assignment statement can assign to multiple targets:: x = y = z = 0 To do the same with assignment expressions, they must be parenthesized:: assert 0 == (x := (y := (z := 0))) Augmented assignment is not supported in expression form:: >>> x +:= 1 File "<stdin>", line 1 x +:= 1 ^ SyntaxError: invalid syntax Otherwise, the semantics of assignment are unchanged by this proposal. Alterations to comprehensions ----------------------------- The current behaviour of list/set/dict comprehensions and generator expressions has some edge cases that would behave strangely if an assignment expression were to be used. Therefore the proposed semantics are changed, removing the current edge cases, and instead altering their behaviour *only* in a class scope. As of Python 3.7, the outermost iterable of any comprehension is evaluated in the surrounding context, and then passed as an argument to the implicit function that evaluates the comprehension. Under this proposal, the entire body of the comprehension is evaluated in its implicit function. Names not assigned to within the comprehension are located in the surrounding scopes, as with normal lookups. As one special case, a comprehension at class scope will **eagerly bind** any name which is already defined in the class scope. A list comprehension can be unrolled into an equivalent function. With Python 3.7 semantics:: numbers = [x + y for x in range(3) for y in range(4)] # Is approximately equivalent to def <listcomp>(iterator): result = [] for x in iterator: for y in range(4): result.append(x + y) return result numbers = <listcomp>(iter(range(3))) Under the new semantics, this would instead be equivalent to:: def <listcomp>(): result = [] for x in range(3): for y in range(4): result.append(x + y) return result numbers = <listcomp>() When a class scope is involved, a naive transformation into a function would prevent name lookups (as the function would behave like a method). class X: names = ["Fred", "Barney", "Joe"] prefix = "> " prefixed_names = [prefix + name for name in names] With Python 3.7 semantics, this will evaluate the outermost iterable at class scope, which will succeed; but it will evaluate everything else in a function:: class X: names = ["Fred", "Barney", "Joe"] prefix = "> " def <listcomp>(iterator): result = [] for name in iterator: result.append(prefix + name) return result prefixed_names = <listcomp>(iter(names)) The name ``prefix`` is thus searched for at global scope, ignoring the class name. Under the proposed semantics, this name will be eagerly bound, being approximately equivalent to:: class X: names = ["Fred", "Barney", "Joe"] prefix = "> " def <listcomp>(prefix=prefix): result = [] for name in names: result.append(prefix + name) return result prefixed_names = <listcomp>() With list comprehensions, this is unlikely to cause any confusion. With generator expressions, this has the potential to affect behaviour, as the eager binding means that the name could be rebound between the creation of the genexp and the first call to ``next()``. It is, however, more closely aligned to normal expectations. The effect is ONLY seen with names that are looked up from class scope; global names (eg ``range()``) will still be late-bound as usual. One consequence of this change is that certain bugs in genexps will not be detected until the first call to ``next()``, where today they would be caught upon creation of the generator. TODO: Discuss the merits and costs of amelioration proposals. Recommended use-cases ===================== Simplifying list comprehensions ------------------------------- These list comprehensions are all approximately equivalent:: # Calling the function twice stuff = [[f(x), x/f(x)] for x in range(5)] # External helper function def pair(x, value): return [value, x/value] stuff = [pair(x, f(x)) for x in range(5)] # Inline helper function stuff = [(lambda y: [y,x/y])(f(x)) for x in range(5)] # Extra 'for' loop - potentially could be optimized internally stuff = [[y, x/y] for x in range(5) for y in [f(x)]] # Iterating over a genexp stuff = [[y, x/y] for x, y in ((x, f(x)) for x in range(5))] # Expanding the comprehension into a loop stuff = [] for x in range(5): y = f(x) stuff.append([y, x/y]) # Wrapping the loop in a generator function def g(): for x in range(5): y = f(x) yield [y, x/y] stuff = list(g()) # Using a mutable cache object (various forms possible) c = {} stuff = [[c.update(y=f(x)) or c['y'], x/c['y']] for x in range(5)] # Using a temporary name stuff = [[y := f(x), x/y] for x in range(5)] If calling ``f(x)`` is expensive or has side effects, the clean operation of the list comprehension gets muddled. Using a short-duration name binding retains the simplicity; while the extra ``for`` loop does achieve this, it does so at the cost of dividing the expression visually, putting the named part at the end of the comprehension instead of the beginning. Capturing condition values -------------------------- Assignment expressions can be used to good effect in the header of an ``if`` or ``while`` statement:: # Current Python, not caring about function return value while input("> ") != "quit": print("You entered a command.") # Current Python, capturing return value - four-line loop header while True: command = input("> "); if command == "quit": break print("You entered:", command) # Proposed alternative to the above while (command := input("> ")) != "quit": print("You entered:", command) # Capturing regular expression match objects # See, for instance, Lib/pydoc.py, which uses a multiline spelling # of this effect if match := re.search(pat, text): print("Found:", match.group(0)) # Reading socket data until an empty string is returned while data := sock.read(): print("Received data:", data) Particularly with the ``while`` loop, this can remove the need to have an infinite loop, an assignment, and a condition. It also creates a smooth parallel between a loop which simply uses a function call as its condition, and one which uses that as its condition but also uses the actual value. Rejected alternative proposals ============================== Proposals broadly similar to this one have come up frequently on python-ideas. Below are a number of alternative syntaxes, some of them specific to comprehensions, which have been rejected in favour of the one given above. Alternative spellings --------------------- Broadly the same semantics as the current proposal, but spelled differently. 1. ``EXPR as NAME``, with or without parentheses:: stuff = [[f(x) as y, x/y] for x in range(5)] Omitting the parentheses in this form of the proposal introduces many syntactic ambiguities. Requiring them in all contexts leaves open the option to make them optional in specific situations where the syntax is unambiguous (cf generator expressions as sole parameters in function calls), but there is no plausible way to make them optional everywhere. With the parentheses, this becomes a viable option, with its own tradeoffs in syntactic ambiguity. Since ``EXPR as NAME`` already has meaning in ``except`` and ``with`` statements (with different semantics), this would create unnecessary confusion or require special-casing. 2. Adorning statement-local names with a leading dot:: stuff = [[(f(x) as .y), x/.y] for x in range(5)] # with "as" stuff = [[(.y := f(x)), x/.y] for x in range(5)] # with ":=" This has the advantage that leaked usage can be readily detected, removing some forms of syntactic ambiguity. However, this would be the only place in Python where a variable's scope is encoded into its name, making refactoring harder. This syntax is quite viable, and could be promoted to become the current recommendation if its advantages are found to outweigh its cost. 3. Adding a ``where:`` to any statement to create local name bindings:: value = x**2 + 2*x where: x = spam(1, 4, 7, q) Execution order is inverted (the indented body is performed first, followed by the "header"). This requires a new keyword, unless an existing keyword is repurposed (most likely ``with:``). See PEP 3150 for prior discussion on this subject (with the proposed keyword being ``given:``). Special-casing conditional statements ------------------------------------- One of the most popular use-cases is ``if`` and ``while`` statements. Instead of a more general solution, this proposal enhances the syntax of these two statements to add a means of capturing the compared value:: if re.search(pat, text) as match: print("Found:", match.group(0)) This works beautifully if and ONLY if the desired condition is based on the truthiness of the captured value. It is thus effective for specific use-cases (regex matches, socket reads that return `''` when done), and completely useless in more complicated cases (eg where the condition is ``f(x) < 0`` and you want to capture the value of ``f(x)``). It also has no benefit to list comprehensions. Advantages: No syntactic ambiguities. Disadvantages: Answers only a fraction of possible use-cases, even in ``if``/``while`` statements. Special-casing comprehensions ----------------------------- Another common use-case is comprehensions (list/set/dict, and genexps). As above, proposals have been made for comprehension-specific solutions. 1. ``where``, ``let``, or ``given``:: stuff = [(y, x/y) where y = f(x) for x in range(5)] stuff = [(y, x/y) let y = f(x) for x in range(5)] stuff = [(y, x/y) given y = f(x) for x in range(5)] This brings the subexpression to a location in between the 'for' loop and the expression. It introduces an additional language keyword, which creates conflicts. Of the three, ``where`` reads the most cleanly, but also has the greatest potential for conflict (eg SQLAlchemy and numpy have ``where`` methods, as does ``tkinter.dnd.Icon`` in the standard library). 2. ``with NAME = EXPR``:: stuff = [(y, x/y) with y = f(x) for x in range(5)] As above, but reusing the `with` keyword. Doesn't read too badly, and needs no additional language keyword. Is restricted to comprehensions, though, and cannot as easily be transformed into "longhand" for-loop syntax. Has the C problem that an equals sign in an expression can now create a name binding, rather than performing a comparison. Would raise the question of why "with NAME = EXPR:" cannot be used as a statement on its own. 3. ``with EXPR as NAME``:: stuff = [(y, x/y) with f(x) as y for x in range(5)] As per option 2, but using ``as`` rather than an equals sign. Aligns syntactically with other uses of ``as`` for name binding, but a simple transformation to for-loop longhand would create drastically different semantics; the meaning of ``with`` inside a comprehension would be completely different from the meaning as a stand-alone statement, while retaining identical syntax. Regardless of the spelling chosen, this introduces a stark difference between comprehensions and the equivalent unrolled long-hand form of the loop. It is no longer possible to unwrap the loop into statement form without reworking any name bindings. The only keyword that can be repurposed to this task is ``with``, thus giving it sneakily different semantics in a comprehension than in a statement; alternatively, a new keyword is needed, with all the costs therein. Migration path ============== The semantic changes to list/set/dict comprehensions, and more so to generator expressions, may potentially require migration of code. In many cases, the changes simply make legal what used to raise an exception, but there are some edge cases that were previously legal and are not, and a few corner cases with altered semantics. Yield inside comprehensions --------------------------- As of Python 3.7, the outermost iterable in a comprehension is permitted to contain a 'yield' expression. If this is required, the iterable (or at least the yield) must be explicitly elevated from the comprehension:: # Python 3.7 def g(): return [x for x in [(yield 1)]] # With PEP 572 def g(): sent_item = (yield 1) return [x for x in [sent_item]] This more clearly shows that it is g(), not the comprehension, which is able to yield values (and is thus a generator function). The entire comprehension is consistently in a single scope. Name lookups in class scope --------------------------- A comprehension inside a class previously was able to 'see' class members ONLY from the outermost iterable. Other name lookups would ignore the class and potentially locate a name at an outer scope:: pattern = "<%d>" class X: pattern = "[%d]" numbers = [pattern % n for n in range(5)] In Python 3.7, ``X.numbers`` would show angle brackets; with PEP 572, it would show square brackets. Maintaining the current behaviour here is best done by using distinct names for the different forms of ``pattern``, as would be the case with functions. Generator expression bugs can be caught later --------------------------------------------- Certain types of bugs in genexps were previously caught more quickly. Some are now detected only at first iteration:: gen = (x for x in rage(10)) # NameError gen = (x for x in 10) # TypeError (not iterable) gen = (x for x in range(1/0)) # Exception raised during evaluation This brings such generator expressions in line with a simple translation to function form:: def <genexp>(): for x in rage(10): yield x gen = <genexp>() # No exception yet tng = next(gen) # NameError To detect these errors more quickly, ... TODO. Open questions ============== Can the outermost iterable still be evaluated early? ---------------------------------------------------- As of Python 3.7, the outermost iterable in a genexp is evaluated early, and the result passed to the implicit function as an argument. With PEP 572, this would no longer be the case. Can we still, somehow, evaluate it before moving on? One possible implementation would be:: gen = (x for x in rage(10)) # translates to def <genexp>(): iterable = iter(rage(10)) yield None for x in iterable: yield x gen = <genexp>() next(gen) This would pump the iterable up to just before the loop starts, evaluating exactly as much as is evaluated outside the generator function in Py3.7. This would result in it being possible to call ``gen.send()`` immediately, unlike with most generators, and may incur unnecessary overhead in the common case where the iterable is pumped immediately (perhaps as part of a larger expression). Frequently Raised Objections ============================ Why not just turn existing assignment into an expression? --------------------------------------------------------- C and its derivatives define the ``=`` operator as an expression, rather than a statement as is Python's way. This allows assignments in more contexts, including contexts where comparisons are more common. The syntactic similarity between ``if (x == y)`` and ``if (x = y)`` belies their drastically different semantics. Thus this proposal uses ``:=`` to clarify the distinction. This could be used to create ugly code! --------------------------------------- So can anything else. This is a tool, and it is up to the programmer to use it where it makes sense, and not use it where superior constructs can be used. With assignment expressions, why bother with assignment statements? ------------------------------------------------------------------- The two forms have different flexibilities. The ``:=`` operator can be used inside a larger expression; the ``=`` operator can be chained more conveniently, and closely parallels the inline operations ``+=`` and friends. The assignment statement is a clear declaration of intent: this value is to be assigned to this target, and that's it. Acknowledgements ================ The author wishes to thank Guido van Rossum and Nick Coghlan for their considerable contributions to this proposal, and to members of the core-mentorship mailing list for assistance with implementation. References ========== .. [1] Proof of concept / reference implementation (https://github.com/Rosuav/cpython/tree/assignment-expressions) Copyright ========= This document has been placed in the public domain. .. Local Variables: mode: indented-text indent-tabs-mode: nil sentence-end-double-space: t fill-column: 70 coding: utf-8 End:

On 11 April 2018 at 06:32, Chris Angelico <rosuav@gmail.com> wrote:
Surely "names" would also be eagerly bound, for use in the "for" loop? [...]
Related objection - when used to name subexpressions in a comprehension (one of the original motivating use cases for this proposal), this introduces an asymmetry which actually makes the comprehension harder to read. As a result, it's quite possible that people won't want to use assignment expressions in this case, and the use case of precalculating expensive but multiply used results in comprehensions will remain unanswered. I think the response here is basically the same as the above - if you don't like them, don't use them. But I do think the additional nuance of "we might not have solved the original motivating use case" is worth a specific response. Overall, I like this much better than the previous proposal. I'm now +1 on the semantic changes to comprehensions, and barely +0 on the assignment expression itself (I still don't think assignment expressions are worth it, and I worry about the confusion they may cause for beginners in particular). Paul

On Wed, Apr 11, 2018 at 6:55 PM, Paul Moore <p.f.moore@gmail.com> wrote:
Yep, exactly. Have corrected the example, thanks.
The PEP has kinda pivoted a bit since its inception, so I'm honestly not sure what "original motivating use case" matters. :D I'm just lumping all the use-cases together at the same priority now.
Now that they have the same semantics as any other form of assignment, they're a bit less useful in some cases, a bit more useful in others, and a lot easier to explain. The most confusing part, honestly, is "why do we have two ways to do assignment", which is why that is specifically answered in the PEP. ChrisA

Overall, I'm slightly negative on this. I think named expressions will be a good thing to have, but not in this form. I'll say up front that, being fully aware of the issues surrounding the introduction of a new keyword, something like a let expression in Haskell would be more readable than embedded assignments in most cases. In the end, I suspect my `let` proposal is a nonstarter and just useful to list with the rest of the rejected alternatives, but I wanted.
Abstract ========
[...]
Rationale =========
[...]
# Even complex expressions can be built up piece by piece y = ((eggs := spam()), (cheese := eggs.method()), cheese[eggs])
I find the assignments make it difficult to pick out what the final expression looks like. The first isn't too bad, but it took me a moment to figure out what y was. Quick: is it * (a, b, c) * (a, (b, c)) * ((a, b), c) * something else First I though it was (a, b, c), then I thought it was actually ((a, b), c), before carefully counting the parentheses showed that I was right the first time. These would be clearer if you could remove the assignment from the expression itself. Assuming "let" were available as a keyword, x = (let eggs = spam().ham in "default" if eggs is None else eggs) y = (let eggs = spam(), cheese = eggs.method() in (eggs, cheese, cheese[eggs])) Allowing for differences in how best to format such an expression, the final expression is clearly separate from its component assignment. (More on this in the Alternative Spellings section below.)
There's no rationale given for why this must be parenthesized. If := were right-associative, assert 0 == (x := y := z := 0) would work fine. (With high enough precedence, the remaining parentheses could be dropped, but one would probably keep them for clarity.) I think you need to spell out its associativity and precedence in more detail, and explain why the rationale for the choice made.
There's no reason give for why this is invalid. I assume it's a combination of 1) Having both += and +:=/:+= would be redundant and 2) not wanting to add 11+ new operators to the language.
Otherwise, the semantics of assignment are unchanged by this proposal.
[List comprehensions deleted]
[existing alternatives redacted]
# Using a temporary name stuff = [[y := f(x), x/y] for x in range(5)]
Again, this would be clearer if the assignment were separated from the expression where it would be used. stuff = [let y = f(x) in [y, x/y] for x in range(5)]
These are the most compelling examples so far, doing the most to push me towards a +1. In particular, my `let` expression is too verbose here: while let data = sock.read() in data: print("Received data:", data) I have an idea in the back of my head about `NAME := FOO` being syntactic sugar for `let NAME = FOO in FOO`, but it's not well thought out.
4. Adding a ``let`` expression to create local bindings value = let x = spam(1, 4, 7, q) in x**2 + 2*x 5. Adding a ``where`` expression to create local bindings: value = x**2 + 2*x where x = spam(1, 4, 7, q) Both have the extra-keyword problem. Multiple bindings are little harder to add than they would be with the ``where:`` modifier, although a few extra parentheses and judicious line breaks make it not so bad to allow a comma-separated list, as shown in my first example at the top of this reply.
4. `` let NAME = EXPR1 in EXPR2``:: stuff = [let y = f(x) in (y, x/y) for x in range(5)] I don't have anything new to say about this. It has the same keyword objections as similar proposals, and I think I've addressed the use case elsewhere.
I don't find this convincing. I don't really see chained assignments often enough to worry about how they are written, plus note my earlier question about the precedence and associativity of :=. The fact is, `x := 5` as an expression statement appears equivalent to the assignment statement `x = 5`, so I suspect people will start using it as such no matter how strongly you suggest they shouldn't. -- Clint

On 11 April 2018 at 13:23, Clint Hepner <clint.hepner@gmail.com> wrote:
# Even complex expressions can be built up piece by piece y = ((eggs := spam()), (cheese := eggs.method()), cheese[eggs])
This is a reasonable concern, IMO. But it comes solidly under the frequently raised objection "This could be used to create ugly code!". Writing it as y = ( (eggs := spam()), (cheese := eggs.method()), cheese[eggs] ) makes it obvious what the structure is. Paul

On Wed, Apr 11, 2018 at 10:23 PM, Clint Hepner <clint.hepner@gmail.com> wrote:
I have no idea what the "in" keyword is doing here, but somehow it isn't being used for the meaning it currently has in Python. Does your alternative require not one but *two* new keywords?
It's partly because of other confusing possibilities, such as its use inside, or capturing, a lambda function. I'm okay with certain forms requiring parens.
And 3) there's no point. Can you give an example of where you would want an expression form of augmented assignment?
Both also have the problem of "exactly how local ARE these bindings?", and the 'let' example either requires two new keywords, or requires repurposing 'in' to mean something completely different from its usual 'item in collection' boolean check. The 'where' example is broadly similar to rejected alternative 3, except that you're removing the colon and the suite, which means you can't create more than one variable without figuring some way to parenthesize. Let's suppose this were defined as: EXPR where NAME = EXPR as a five-component sequence. If you were to write this twice EXPR where NAME = EXPR where OTHERNAME = EXPR then it could just as logically be defined as "EXPR where NAME = (EXPR where OTHERNAME = EXPR)" as the other way. And even if it were to work as "(EXPR where NAME = EXPR) where OTHERNAME = EXPR", that still has the highly confusing semantics of being evaluated right-to-left. (Before you ask: no, you can't define it as "EXPR where NAME = EXPR , NAME = EXPR", because that would require looking a long way forward.)
This section is specifically about proposals that ONLY solve this problem within list comprehensions. I don't think there's any point mentioning your proposal there, as the "let NAME = EXPR in EXPR" notation has nothing particularly to do with comprehensions.
If you don't use them, why would you care either way? :)
Probably. But when they run into problems, the solution will be "use an assignment statement, don't abuse the assignment expression". If you want to, you can write this: np = __import__("numpy") But it's much better to use the import statement, and people will rightly ask why you're using that expression form. Your "let... in" syntax is kinda interesting, but has a number of problems. Is that the exact syntax used in Haskell, and if so, does Haskell use 'in' to mean anything else in other contexts? ChrisA

On 11 April 2018 at 14:25, Chris Angelico <rosuav@gmail.com> wrote:
The only possible reading of x := y := z := 0 is as x := (y := (z := 0)) because an assignment expression isn't allowed on the LHS of :=. So requiring parentheses is unnecessary. In the case of an assignment statement, "assignment to multiple targets" is a special case, because assignment is a statement not an expression. But with assignment *expressions*, a := b := 0 is simply assigning the result of the expression b := 0 (which is 0) to a. No need for a special case - so enforced parentheses would *be* the special case. And you can't really argue that they are needed "for clarity" at the same time as having your comments about how "being able to write ugly code" isn't a valid objection :-) Paul

On Wed, Apr 11, 2018 at 11:37 PM, Paul Moore <p.f.moore@gmail.com> wrote:
Sure, if you're just assigning zero to everything. But you could do that with a statement. What about this: q = { lambda: x := lambda y: z := a := 0, } Yes, it's an extreme example, but look at all those colons and tell me if you can figure out what each one is doing. ChrisA

On 11 April 2018 at 14:54, Chris Angelico <rosuav@gmail.com> wrote:
lambda: x := (lambda y: (z := (a := 0))) As I say, it's the only *possible* parsing. It's ugly, and it absolutely should be parenthesised, but there's no need to make the parentheses mandatory. (And actually, it didn't take me long to add those parentheses, it's not *hard* to parse correctly - for a human). Paul

On Thu, Apr 12, 2018 at 12:11 AM, Paul Moore <p.f.moore@gmail.com> wrote:
Did you pick up on the fact that this was actually in a set? With very small changes, such as misspelling "lambda" at the beginning, this actually becomes a dict display. How much of the expression do you need to see before you can be 100% sure of the parsing? Could you do this if fed tokens one at a time, with permission to look no more than one token ahead? ChrisA

On 11 April 2018 at 15:28, Chris Angelico <rosuav@gmail.com> wrote:
Yes. It's not relevant to the parsing. It is relevant to the possibility of errors, as you point out. But once again, it's not the role of the PEP to prevent people writing bad code. Anyway, this is mostly nitpicking. I'm not trying to argue that this is good code, or robust code, or even code that I'd permit within a mile of one of my programs. All I'm trying to say is that *if* you want to state that the parentheses are mandatory in chained assignment expressions, then I think you need to justify it (and my suspicion is that you don't have a good justification other than "it prevents bad code" - which is already covered by the part of the PEP that points out that it's not the job of this PEP to prevent people writing bad code ;-)). Personally, I dislike the tendency with recent syntax proposals to mandate parentheses "to remove ambiguity". In my experience, all that happens is that I end up never knowing whether parentheses are required or not - and as a result end up with too many parentheses, making my code look ugly and encouraging cargo cult style "better add parens just in case" behaviour (as opposed to the reasonable rule "add parens for readability"). Paul

On Thu, Apr 12, 2018 at 12:28:23AM +1000, Chris Angelico wrote:
I agree with Paul, except I think he's added too many parens. Chained assignments ought to be obvious enough that we can dispense with the extras: lambda: x := (lambda y: (z := a := 0)) I know that they are legal, but I really dislike *pointless* examples that bind to a name and then never use it. If we're to get a good feel for how complex these expressions are going to be, they ought to be realistic -- even if that makes them more complex. And I'm not terribly disturbed by excessively obfuscated examples. The answer to obfuscated code is, Don't Do That. So we should consider complex examples which are *realistic*, not ones designed intentionally as obfuscated code. So, with that advice, let's take your q example from above, and re-work it into something which is at least potentially realistic, of a sort. We want q to be a set consisting of a factory function which takes a single argument (different from your example, I know), builds an inner function, then returns that function and the result of that function called with the original argument: def factory(arg): def inner(y): a := z := y + 1 # seems kinda pointless to me, but okay... return (a, a+z, a*z) return (inner, inner(arg)) q = {1, 2, factory, 3, 4} Now let's re-write it in using expression assignment: q = {1, 2, (lambda arg: lambda y: (a := (z := y + 1), a+z, z*z) ), 3, 4, } Not too awful, although it is kinda pointless and not really a great justification for the feature. Let's obfuscate it: q = {1, 2, (lambda arg: lambda y: a := z := y + 1, a+z, z*z), 3, 4} I've seen worse :-) -- Steve

Just one; I don't see using ``in`` here to be any more or a problem than was reusing ``if``, ``then``, and ``else`` for conditional expressions. Its purpose is to separate the bindings from the expression they are used in; eggs and cheese are only valid in the expression that follows "in". Syntactically, this is pretty much identical to Haskell's "let" expression, but the idea of an expression with local bindings is much older. (https://en.wikipedia.org/wiki/Let_expression)
I wouldn't want one :). I'm just suggesting that the PEP include something to the effect of "We're not adding augmented assignment expressions because...".
``in`` already has two different uses: as a Boolean operator (two, actually, with ``not in``) and as part of the various ``for`` constructs. IMO, I don't see adding this third meaning to be a problem. With ``let``, the scope extends as far right of ``in` as possible: let NAME = EXPR in let OTHERNAME = EXPR in EXPR is equivalent to let NAME = EXPR in (let OTHERNAME = EXPR in EXPR)
I agree that `where` *should* be rejected; I don't really like them in Haskell, either. I only listed ``let`` here because I assume it *will* be rejected due to its requiring a new keyword, no matter how much I think adding a new keyword is warranted.
Fair enough, although that suggests a proper let expression precludes the need for any special handling.
I just mean this seems like a weak argument if you are trying to convince someone to use assignment statements. "Assuming I never use chained assignments, why should I use ``=`` instead of ``:=`?"
All the dunder methods exist to support a higher-level syntax, and are not really intended to be used directly, so I think a stronger argument than "Don't use this, even though you could" would be preferable.
I don't believe ``in`` is used elsewhere in Haskell, although Python already has at least two distinct uses as noted earlier. In Haskell, the ``let`` expression (like much of Haskell's syntax) is syntactic sugar for an application of lambda abstraction. The general form let n1 = e1 n2 = e2 in e3 is syntactic sugar for let n1 = e1 in let n2 = e2 in e3 where multiple bindings are expanded to a series of nested expressions. The single expression let n1 = e1 in e2 itself is transformed into (\n1 -> e2) e1 (or translated to Python, (lambda n1: e2)(e1)). -- Clint

On Thu, Apr 12, 2018 at 12:46 AM, Clint Hepner <clint.hepner@gmail.com> wrote:
A 'for' loop has the following structure: for targets in expr: To the left of the 'in', you have a list of assignment targets. You can't assign to anything with an 'in' in it:
(Removing the parentheses makes this "for x in (y in [1])", which is perfectly legal, but has no bearing on this discussion.) In contrast, the way you're using it here, it's simply between two arbitrary expressions. There's nothing to stop you from using the 'in' operator on both sides of it: let x = (a in b) in (b in c) This would make the precedence tables extremely complicated, or else have some messy magic to make this work.
Does the document really need to say that it isn't needed?
In round 3 of this PEP, I was focusing on listing all plausible variants. I'm now focusing more on an actually-viable proposal, so myriad alternatives aren't as important any more.
Fair enough. The most important part is the declaration of intent. By using an assignment *statement*, you're clearly showing that this was definitely intentional.
Makes sense. And if someone actually wants expression-local name bindings, this is the one obvious way to do it (modulo weirdness around class scope). This would not solve the if/while situation, and it wouldn't solve several of the other problems, but it does have the advantage of logically being expression-local. ChrisA

On 2018-04-11 05:23, Clint Hepner wrote:
I find the assignments make it difficult to pick out what the final expression looks like.
I strongly agree with this, and for me I think this is enough to push me to -1 on the whole proposal. For me the classic example case is still the quadratic formula type of thing: x1, x2 = (-b + sqrt(b**2 - 4*a*c))/2, (-b - sqrt(b**2 - 4*a*c))/2 It just doesn't seem worth it to me to create an expression-level assignment unless it can make things like this not just less verbose but at the same time more readable. I don't consider this more readable: x1, x2 = (-b + sqrt(D := b**2 - 4*a*c)))/2, (-b - sqrt(D))/2 . . . because having to put the assignment inline creates a visual asymmetry, when for me the entire goal of an expression-level statement is to make the symmetry between such things MORE obvious. I want to be able to write: x1, x2 = (-b + sqrt(D)))/2, (-b - sqrt(D))/2 ... . . . where "..." stands for "the part of the expression where I define the variables I'm re-using in multiple places in the expression". The new proposal does at least have the advantage that it would help with things like this: while x := some_function_call(): # do stuff So maybe I'm -0.5 rather than -1. But it's not just that this proposal "could be used to create ugly code". It's that using it for expression-internal assignments WILL create ugly code, and there's no way to avoid it. I just don't see how this proposal provides any way to make things like the quadratic formula example above MORE readable. -- Brendan Barnwell "Do not follow where the path may lead. Go, instead, where there is no path, and leave a trail." --author unknown

On 2018-04-11 11:05, David Mertz wrote:
That's clever, but why bother? I can already do this with existing Python: D = b**2 - 4*a*c x1, x2 = (-b + sqrt(D)))/2, (-b - sqrt(D))/2 If the new feature encourages people to do something like your example (or my earlier examples with the D definition inline in the expression for x1), then I'd consider that another mark against it. -- Brendan Barnwell "Do not follow where the path may lead. Go, instead, where there is no path, and leave a trail." --author unknown

Fair enough. I wouldn't actually do what I suggested either. But then, I also wouldn't ever write: x1, x2 = (-b + sqrt(D)))/2, (-b - sqrt(D))/2 [with/where/whatever D=...] If your goal is simply to have symmetry in the plus-or-minus clauses, I was simply pointing out you can have that with the ':=' syntax. Inasmuch as I might like assignment expressions, it would only be in while or if statements, personally. On Wed, Apr 11, 2018, 5:09 PM Brendan Barnwell <brenbarn@brenbarn.net> wrote:

I really like this proposal in the context of `while` loops but I'm lukewarm in other contexts. I specifically like what this would do for repeated calls. Being able to replace all of these md5 = hashlib.md5() with open(filename, 'rb') as file_reader: for chunk in iter(lambda: file_reader.read(1024), b''): md5.update(chunk) md5 = hashlib.md5() with open(filename, 'rb') as file_reader: while True: chunk = file_reader.read(1024) if not chunk: break md5.update(chunk) md5 = hashlib.md5() with open(filename, 'rb') as file_reader: chunk = file_reader.read(1024) while chunk: md5.update(chunk) chunk = file_reader.read(1024) with md5 = hashlib.md5() with open(filename, 'rb') as file_reader: while chunk := file_reader.read(1024): md5.update(chunk) seems really nice. I'm not sure the other complexity is justified by this nicety and I'm really wary of anything that makes comprehensions more complicated; I already see enough comprehension abuse to the point of illegibility. --George On Wed, Apr 11, 2018 at 10:51 AM Brendan Barnwell <brenbarn@brenbarn.net> wrote:

Le 11/04/2018 à 23:34, George Leslie-Waksman a écrit :
I like the new syntax, but you can already do what you want with iter(): md5 = hashlib.md5() with open('/etc/fstab', 'rb') as file_reader: for chunk in iter(lambda: file_reader.read(1024), b''): md5.update(chunk) Anyway, both use case fall short IRL, because you would wrap read in huge try/except to deal with the mess that is letting a user access the filesystem.

On Thu, Apr 12, 2018 at 4:45 PM, Michel Desmoulin <desmoulinmichel@gmail.com> wrote:
That works ONLY if you're trying to check for a sentinel condition via equality. So that'll work for the file read situation, but it won't work if you're watching for any negative number (from APIs that use negative values to signal failure), nor something where you want the condition to be "is not None", etc, etc, etc. Also, it doesn't read nearly as well. ChrisA

On Thu, Apr 12, 2018 at 3:49 AM, Brendan Barnwell <brenbarn@brenbarn.net> wrote:
What if you want to use it THREE times? roots = [((-b + sqrt(D))/2/a, (-b - sqrt(D))/2/a) for a,b,c in triangles if (D := b**2 - 4*a*c) >= 0] Now it's matching again, without any language changes. (I've reinstated the omitted division by 'a', in case anyone's confused by the translation. It has no bearing on the PEP discussion.) Same if you're using an if statement.
I don't think it's as terrible as you're saying. You've picked a specific example that is ugly; okay. This new syntax is not meant to *replace* normal assignment, but to complement it. There are times when it's much better to use the existing syntax. ChrisA

On Wed, Apr 11, 2018 at 1:49 PM, Brendan Barnwell <brenbarn@brenbarn.net> wrote:
I'd probably write this as: x1, x2 = [(-b + s*sqrt(b**2 - 4*a*c))/(2*a) for s in (1,-1)] Agreed that the PEP doesn't really help for this use case, but I don't think it has to. The main use cases in the PEP seem compelling enough to me. Nathan

Great work Chris! Thank you! I do not know whether this is good or bad, but this PEP considers so many different topics, although closely interrelated with each other. 2018-04-11 8:32 GMT+03:00 Chris Angelico <rosuav@gmail.com>:
I think this change is important one no matter what will be the future of the current PEP. And since it breaks backward compatibility it deserves a separate PEP.
Previously, there was an alternative _operator form_ `->` proposed by Steven D'Aprano. This option is no longer considered? I see several advantages with this variant: 1. It does not use `:` symbol which is very visually overloaded in Python. 2. It is clearly distinguishable from the usual assignment statement and it's `+=` friends There are others but they are minor.
But the ugly code matters, especially when it comes to Python. For me, the ideal option would be the combination of two rejected parts: Special-casing conditional statements
(+ in `while`) combined with this part: 3. ``with EXPR as NAME``::
I see no benefit to have the assignment expression in other places. And all your provided examples use `while` or `if` or some form of comprehension. I also see no problem with `if (re.search(pat, text) as match) is not None:..`. What is the point of overloading language with expression that will be used only in `while` and `if` and will be rejected by style checkers in other places? With kind regards, -gdg

On Wed, Apr 11, 2018 at 11:03 PM, Kirill Balunov <kirillbalunov@gmail.com> wrote:
Well, it was Guido himself who started the sub-thread about classes and comprehensions :) To be honest, the changes to comprehensions are mostly going to be under-the-hood tweaks. The only way you'll ever actually witness the changes are if you: 1) Use assignment expressions inside comprehensions (ie using both halves of this PEP); or 2) Put comprehensions at class scope (not inside methods, but actually at class scope), referring to other names from class scope, in places other than in the outermost iterable 3) Use 'yield' expressions in the outermost iterable of a list comprehension inside a generator function 4) Create a generator expression that refers to an external name, then change what that name is bound to before pumping the generator; depending on the one open question, this may occur ONLY if this external name is located at class scope. 5) Use generator expressions without iterating over them, in situations where iterating might fail (again, depends on the one open question). Aside from the first possibility, these are extremely narrow edge and corner cases, and the new behaviour is generally the more intuitive anyway. Class scope stops being so incredibly magical that it's completely ignored, and now becomes mildly magical such that name lookups are resolved eagerly instead of lazily; and the outermost iterable stops being magical in that it defies the weirdness of class scope and the precise definitions of generator functions. Special cases are being removed, not added.
I'm not sure why you posted this in response to the open question, but whatever. The arrow operator is already a token in Python (due to its use in 'def' statements) and should not conflict with anything; however, apart from "it looks different", it doesn't have much to speak for it. The arrow faces the other way in languages like Haskell, but we can't use "<-" in Python due to conflicts with "<" and "-" as independent operators.
Can you give an example of how your syntax is superior to the more general option of simply allowing "as" bindings in any location? ChrisA

2018-04-11 16:50 GMT+03:00 Chris Angelico <rosuav@gmail.com>:
Can you give an example of how your syntax is superior to the more general option of simply allowing "as" bindings in any location?
This is not my syntax :) And not even my idea. I just do not understand, and even a little skeptical about allowing "as" bindings in **any location** with global scoping. All the examples in this thread and the previous ones, as well as almost all PEP's examples show how this feature will be useful in `if`, `while` statements and comprehension/generator expressions. And it excellently solves this problem. This feature increases the capabilities of these statements and also positively affects the readability of the code and it seems to me that everyone understands what this means in this context without ambiguity in their meaning in `while` or `with` statements. The remaining examples (general ones) are far-fetched, and I do not have much desire to discuss them :) These include: lambda: x := lambda y: z := a := 0 y = ((eggs := spam()), (cheese := eggs.method()), cheese[eggs]) and others of these kind... Thus, I do not understand why to solve such a general and complex problem, when this syntax is convenient only in specific cases. In addition, previously the concept of a Statement-Local Name Bindings was discussed, which I basically like (and it fits the above idea). In this version, it was abandoned completely, but it is unclear for what reasons. p.s.: Maybe someone has use-cases outside `if`, `while` and comprehensions, but so far no one has demonstrated them. With kind regards, -gdg

2018-04-11 18:01 GMT+03:00 Kirill Balunov <kirillbalunov@gmail.com>:
I find that I wrote very vague, so I'll try in response to my answer to add some specifics. In general, I find this idea missed in the language and thank you for trying to fix this! In my opinion it has only a meaning in certain constructions such as `while`, `if`, `elif` and maybe comprehensions\generators. As a general form "anywhere" it can be _useful_, but makes the code unreadable and difficult to perceive while giving not so much benefit. What I find nice to have: Extend while statement syntax: while (input("> ") as command) != "quit": print("You entered:", command) Extend if statement syntax: if re.search(pat, text) as match: print("Found:", match.group(0)) if (re.search(pat, text) as match) is not None: print("Found:", match.group(0)) also `elif` clauses should be extended to support. Extend comprehensions syntax: # Since comprehensions have an if clause [y for x in data if (f(x) as y) is not None] # Also this form without `if` clause [(y, x/y) with f(x) as y for x in range(5)] Extend ternary expression syntax: data = y/x if (f(x) as y) > 0 else 0 I think that is all. And it seems to me that it covers 99% of all the use-cases of this feature. In my own world I would like them to make a local _statement_ binding (but this is certainly a very controversial point). I even like that this syntax matches the `with` an `except` statements syntax, although it has a different semantic. But I do not think that anyone will have problems with perception of this. With kind regards, -gdg

On Thu, Apr 12, 2018 at 5:24 AM, Kirill Balunov <kirillbalunov@gmail.com> wrote:
What you're writing there is not truly an extension of the while statement, but a special feature of an expression *within* the while header. Syntactically, this "expr as name" notation must be able to be combined with other operators (as in what you've written here), so it isn't like the way the 'with' or 'import' statement specifically makes a feature available as its own syntax.
Extend ternary expression syntax:
data = y/x if (f(x) as y) > 0 else 0
Again, this is doing further operations after the capturing, so it's not like you can incorporate it in the syntax. You can't write: expr2 if expr1 as NAME else expr3 and explain its semantics that way, because then you can't put the "> 0" part in anywhere. So if, syntactically, this is a modification to expressions in general, why restrict them to certain contexts? Why can't I lift the condition out of the 'while' and give it a name? while (input("> ") as command) != "quit": # becomes # cond = (input("> ") as command) != "quit" print(cond) while cond: But I can't if this is magic in the 'while' statement. What do you gain by forbidding it?
Statement-local names, controversial? You don't say! I actually think the parallel with 'with' and 'except' works *against* that version of the proposal, precisely because of the different semantics (as you mention). The difference between: except Exception as e: except (Exception as e): is significant and fairly easy to spot; as soon as you try to use 'e', you'll figure out that it's the Exception class, not the instance that got thrown. But in a 'with' statement? with open(fn) as f: with (open(fn) as f): These will do the same thing, because Python's file objects return self from __enter__. So do a lot of context managers. You can go a VERY long way down this rabbit-hole, doing things like: with (open(infile) as read and open(outfile, "w") as write): write.write(read.read()) In CPython, you likely won't notice anything wrong here. And hey, it's backslash-free multi-line context management! In fact, you might even be able to use this in *Jython* without noticing a problem. Until you have two output files, and then stuff breaks badly. Thus I sought to outright forbid 'as' in the expressions used in a 'with' or 'except' statement. The problem doesn't exist with ':=', because it's clear that the different semantics go with different syntax: with open(fn) as f: with f := open(fn): And since there's no reason to restrict it, it's open to all contexts where an expression s needed. ChrisA

2018-04-12 1:43 GMT+03:00 Chris Angelico <rosuav@gmail.com>:
All right! You caught me :) For lack of a thoughtful alternative version in my head, let it be an expression but such that it can only be used (evaluated) in boolean context: `while` and `if`s statements (let's skip comprehensions and generators for some time, except their `if` clause). I agree that it contradicts with the way that in 'with' or 'import' statements it is a part of their own syntax. But nonetheless they have the same syntax but, strictly speaking, different semantics! And this is normal, because they are different statements.
Ability to combine `expr as name` with other operators - is actually a matter of operator precedence. In my mind it should have the highest precedence: while input("> ") as command != "quit": is equivalent to while (input("> ") as command) != "quit": Suppose that some function can return `empty tuple` and `None` (which are both False in boolean context), for this case you can write: while func(something) as value is not None: which is equivalent to while (func(something) as value) is not None: and can be also written as: while (func(something) as value) or (value is not None): In the last snippet parenthesis were used only for readability. Another example: while f1(x) + f2(x) as value: will be parsed as: while f1(x) + (f2(x) as value):
Do not quite understand why I can not? :) (the parenthesis was used only for readability)
I gain readability! I don't see any reason to use it in other contexts... Because it makes the code unreadable and difficult to perceive while giving not so much benefit. I may be wrong, but so far I have not seen a single example that at least slightly changed my mind. Concerning your example, I did not understand it..`cond` is evaluated only once...why you need a while loop in this case?
It **will not be allowed** in `except` since there is no boolean context... There is no parallels, only the same syntax, it seems to me that no one will have problems understanding what that means in different contexts. In addition, at the moment all are coping with the differences between `import`, `with` and `except`.
The same goes for `with` statements, I see no reason to use both `:=` and `expr as name` in with statements. How this feature can be used here, especially in the context you mentioned above?
As for me this example just shows that `:=` should not be used in `with` statements at all. I ask you to understand me correctly, but what surprised me most, was that different versions of PEP (3 and 4) speaks about absolutely different things. I think it would be great to have two competing proposals: 1. This PEP in the form that is now (with general assignment expression) 2. Another PEP which discusses only the changes in if and while statements. I understand that it is much easier to advise than to do! I also understand how much time it takes for all this. I myself still can not find the time (English is given to me with great difficulty :)) to write about PEP about partial assignment statement ... But still, as I see these two PEPs will allow to simultaneously look at the two approaches, and also allow to separately work through the problems arising in each of them. I do not want to repeat it again, but I have not yet seen any reasonable example where this current `:=` feature can be useful, except `while` and `if`. So I do not understand why everything needs to be complicated and not concentrate on what can actually be useful. With kind regards, -gdg

Wouldn't these local name bindings make the current "as" clause of "with f(x) as y" completely obsolete ? It's probably good to know my background, and my background is that I know completely nothing of the implementation, im only junior software engineer, and python was my first programming experience, and still the one I have by far the most experience with. To me, the entire proposal sounds mostly like an expansion of the as syntax as we know it from "with". There will be no difference between: with open(filename) as f: // code and with f := open(filename): // code or at least as far as I can see. (that is, if := will be allowed in the with statement, and it sounds like it will ?) from this (outsiders?) point of view, it'd make a lot more sense to keep using "as" as the local binding operator - that's exactly what it already seems to do, even if it looks different under the hood. This would keep it to just one way to do stuff, and that happens to be the way everyone's used to. of course, should still extend. So right now. with open(os.path.join(path, filename) as full_file_path) as f: // Do stuff with both full_file_path and f wont work, but it'd still be awesome/useful if it did. (overall +1 for the idea, for what it's worth) Jacco

2018-04-12 12:48 GMT+03:00 Jacco van Dorp <j.van.dorp@deonet.nl>:
Thank you Jacob! I do not know if I understood correctly how you understand what is happening here. But you are just demonstrating my fears about this proposal... with f := open(filename): This will be only valid if the returned object of (f := open(filename)) defines __enter__ and __exit__ methods ( Nevertheless, in this situation it is so). But in other cases it will raise an error. Generally `with name := expr` is not equivalent to `with expr as name:`. In another places, for example, `except` clause `f := something` is valid only if the returned type is an object, which inherit from BaseException. So in this two situations, in my opinion, it will not be used too much. Yours example under current proposal should look like `with open( full_file_path := os.path.join(path, filename) ) as f:`. With kind regards, -gdg

2018-04-12 13:28 GMT+02:00 Kirill Balunov <kirillbalunov@gmail.com>:
No, it will not raise an error to replace all these "as" usages with name binding, if we choose operator priority right. Even if we consider "with (y := f(x))", the f(x) the with gets is the same as the one bound to the name - that's the point of the binding expression. The only difference seems to be whether the name binding is done before or after the __enter__ method - depending on operator priority (see below). I've looked through PEP 343, contextlib docs ( https://docs.python.org/3/library/contextlib.html ), and I couldn't find a single case where "with (y := f(x))" would be invalid. The only difference I can think of is objects where __enter__ doesn't return self. Inexperienced programmers might forget it, so we're giving them the "as y" part working instead of binding y to None. There might be libraries out there that return a non-self value from __enter__, which would alter behaviour. I honestly can't imagine why you might do that tho. And this could be entirely solved by giving "with" a higher priority than "as". Then if "as" was used instead of ":=", you could just drop "as" as part of the with statement, and it'd work the exact same way everyone's used to. And it's basically the same with "except x as y"; If except gets a higher priority than as, it'd do the same thing(i.e., the exception object gets bound to y, not the tuple of types). At least, that's what it'd look like from outside. If people'd complain "but the local binding does different behind except", you explain the same story as when they would ask about the difference between + and * priority in basic math - and they'd be free to use parenthesis as well if they really want to for some reason i'm unable to comprehend. except (errors := (TypeError, ValueError)) as e: # Or of course: except ( (TypeError, ValueError) as errors ) as e: logger.info(f"Error {e} is included in types: {errors}") Where it all comes together is that if as is chosen instead of :=, it might just be far easier to comprehend for people how it works. "same as in with" might be wrong technically, but it's simple and correct conceptually. Note: I'm talking about operator priority here as if with and except return anything - which I know they don't. That probably complicates it a lot more than it sounds like what I've tried to explain my thoughts here, but I hope I made sense. (it's obvious to me, but well, im dutch..(jk)) Jacco

On 12 April 2018 at 22:22, Jacco van Dorp <j.van.dorp@deonet.nl> wrote:
Consider this custom context manager: @contextmanager def simple_cm(): yield 42 Given that example, the following code: with cm := simple_cm() as value: print(cm.func.__name__, value) would print "'simple_cm 42", since the assignment expression would reference the context manager itself, while the with statement binds the yielded value. Another relevant example would be `contextlib.closing`: that returns the passed in argument from __enter__, *not* self. And that's why earlier versions of PEP 572 (which used the "EXPR as NAME" spelling) just flat out prohibited top level name binding expressions in with statements: "with (expr as name):" and "with expr as name:" were far too different semantically for the only syntactic difference to be a surrounding set of parentheses. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

2018-04-12 15:02 GMT+02:00 Nick Coghlan <ncoghlan@gmail.com>:
Makes sense. However, couldn't you prevent that by giving with priority over the binding ? As in "(with simple_cm) as value", where we consider the "as" as binding operator instead of part of the with statement ? Sure, you could commit suicide by parenthesis, but by default it'd do exactly what the "with simple_cm as value" currently does. This does require use of as instead of :=, though. (which was the point I was trying to make, apologies for the confusion)

On Thu, Apr 12, 2018 at 11:31 PM, Jacco van Dorp <j.van.dorp@deonet.nl> wrote:
If you want this to be a generic name-binding operation, then no; most objects cannot be used as context managers. You'll get an exception if you try to use "with 1 as x:", for instance. As Nick mentioned, there are context managers that return something other than 'self', and for those, "with expr as name:" has an important meaning that cannot easily be captured with an assignment operator. ChrisA

On Thu, Apr 12, 2018 at 7:21 PM, Kirill Balunov <kirillbalunov@gmail.com> wrote:
This is, in effect, your entire argument for permitting assignments only in certain contexts. "I can't think of any useful reason for doing this, so we shouldn't do it". But that means making the language grammar more complicated (both in the technical sense of the parser's definitions, and in the colloquial sense of how you'd explain Python to a new programmer), because there are these magic constructs that can be used anywhere in an expression, but ONLY if that expression is inside an if or while statement. You lose the ability to refactor your code simply to satisfy an arbitrary restriction to appease someone's feeling of "it can't be useful anywhere else". There are basically two clean ways to do this: 1) Create actual syntax as part of the while statement, in the same way that the 'with EXPR as NAME:' statement does. This means you cannot put any additional operators after the 'as NAME' part. It's as much a part of the statement's syntax as the word 'in' is in a for loop. 2) Make this a feature of expressions in general. Then they can be used anywhere that an expression can be. I've gone for option 2. If you want to push for option 1, go ahead, but it's a nerfed solution just because you personally cannot think of any good use for this. ChrisA

On Wed, Apr 11, 2018 at 11:50:44PM +1000, Chris Angelico wrote:
On the contrary, it puts the expression first, where it belongs *semi-wink*. The expression is the most important part of the assignment expression, and because we read from left to right, it should come first. Let's take a simple example: pair = (first_value := x + y + z, a + b + first_value ) What's the first item of the pair? If you're like me, and I think most people are similar, when skimming the code, you read only far across each line to get an idea of whether it is relevant or not. In this case, when skimming, you have to read past the name, past the assignment operator, and only then do you see the relevant information. Contrast: pair = (x + y + z -> first_value, a + b + first_value ) Now you need only read *up to* the assignment operator. Now clearly a careful and detailed reading of the code requires just as much work either way, but we often skim code, especially trying to identify the section that needs careful reading. I know that it is a long standing convention in programming and mathematics to write assignments with the variable on the left, but when I'm sketching out code on paper, I often *start* with the expression: x + y + z and only then do I go back and squeeze in a name binding on the left. (Especially since half the time I'm not really sure what the variable should be called. Naming things is hard.) Or I just draw in an arrow pointing to the name on the right: x + y + z ----> name
The arrow faces the other way in languages like Haskell,
Indeed, but in R, it faces to the right. (Actually, R allows both direction.) There's also apparently a language BETA which uses -> for assignment, although I've never used it. HP calculator "RPN" language also includes a -> assignment operator for binding named parameters (taken off the stack) inside functions, except they use a custom encoding with an arrow symbol, not a literal hyphen+greater-than sign. Likewise for TI Nspire calculators, which also use an right-pointing arrow assignment operator. (They also have a Pascal-style := operator, so you're covered both ways.) This comes from various dialects of calculator BASIC. -- Steve

On Fri, Apr 13, 2018 at 10:22 PM, Steven D'Aprano <steve@pearwood.info> wrote:
The 'as' syntax already has that going for it. What's the advantage of the arrow over the two front-runners, ':=' and 'as'?
Yet Python has an if/else operator that, in contrast to C-inspired languages, violates that rule. So it's not a showstopper. :)
I looked up R's Wikipedia page and saw only the left-facing arrow. How common is the right-facing arrow? Will people automatically associate it with name binding?
So we have calculators, and possibly R, and sorta-kinda Haskell, recommending some form of arrow. We have Pascal and its derivatives recommending colon-equals. And we have other usage in Python, with varying semantics, recommending 'as'. I guess that's enough to put the arrow in as another rejected alternate spelling, but not to seriously consider it. ChrisA

On 13 April 2018 at 22:35, Chris Angelico <rosuav@gmail.com> wrote:
I stumbled across https://www.hillelwayne.com/post/equals-as-assignment/ earlier this week, and I think it provides grounds to reconsider the suitability of ":=", as that symbol has historically referred to *re*binding an already declared name. That isn't the way we're proposing to use it here: we're using it to mean both implicit local variable declaration *and* rebinding of an existing name, the same as we do for "=" and "as". I think the "we already use colons in too many unrelated places" argument also has merit, as we already use the colon as: 1. the header terminator when introducing a nested suite 2. the key:value separator in dictionary displays and comprehensions 3. the name:annotation separator in function parameter declarations 4. the name:annotation separator in variable declarations and assignment statements 5. the parameter:result separator in lambda expressions 6. the start:stop:step separator in slice syntax "as" is at least more consistently associated with name binding, and has fewer existing uses in the first place, but has the notable downside of being thoroughly misleading in with statement header lines, as well as being *so* syntactically unobtrusive that it's easy to miss entirely (especially in expressions that use other keywords). The symbolic "right arrow" operator would be a more direct alternative to the "as" variant that was more visually distinct: # Handle a matched regex if (pattern.search(data) -> match) is not None: ... # More flexible alternative to the 2-arg form of iter() invocation while (read_next_item() -> item) is not None: ... # Share a subexpression between a comprehension filter clause and its output filtered_data = [y for x in data if (f(x) -> y) is not None] # Visually and syntactically unambigous in with statement headers with create_cm() -> cm as enter_result: ... (Pronunciation-wise, if we went with that option, I'd probably pronounce "->" as "as" most of the time, but there are some cases like the "while" example above where I'd pronounce it as "into") The connection with function declarations would be a little tenuous, but could be rationalised as: Given the function declation: def f(...) -> Annotation: ... Then in the named subexpression: (f(...) -> name) the inferred type of "name" is "Annotation" Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Fri, Apr 13, 2018 at 7:36 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
I've not done much research about this topic, but I lived through it (my first languages were Algol-60 And Fortran=IV, in 1974, soon followed by Pascal and Algol-68) and I think that blog post is overly biased by one particular thread of C's ancestry. CPL, BCPL and B were bit players in the world of languages (I suspect mostly focused on Bell Labs and/or MIT, at least US east coast). I should probably blog about my own view of this history, but basically I don't believe that the distinction between initialization and re-assignment is the big decider here. Python historically doesn't care, all its assignments work like dict[key] = value (with a slight exception for the analyses related to local scopes and closures).
But := is not a colon -- it's a new symbol spelled as two characters. The lexer returns it as a single symbol, like it does != and ==. And we're lucky in the sense that no expression or statement can start with =, so there is no context where : = and := would both be legal.
Right -- 'as' signals that there's something funky happening to its left argument before the assignment is made.
I am not excited about (expr -> var) at all, because the existing use of -> in annotations is so entirely different. -- --Guido van Rossum (python.org/~guido)

On Sat, Apr 14, 2018 at 12:36 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
I'm not bothered by that. Assignment semantics vary from one language to another; the fact that Python marks as local anything that's assigned to is independent of the way you assign to it. ("print(x)" followed by "for x in ..." is going to bomb with UnboundLocalError, for instance.) If Python had any form of local variable declarations, it wouldn't change the behaviour of the := operator. ChrisA

On Fri, Apr 13, 2018 at 10:35:59PM +1000, Chris Angelico wrote:
Personally, I like "as" better than -> since English-like expressions and syntax is nicer than symbols. (Up to a point of course -- we surely don't want COBOL-like "ADD 1 TO x" syntax.) The arrow also completely bypasses the entire with/except problem. So my position is: - "as" is more Pythonic and looks nicer; - but it requires a compromise to avoid the with/except problem; - I'm okay with that compromise, but if others aren't, my second preference is the arrow binding operator -> - which also has the advantage that it is completely unused apart from function annotations; - and if people don't like that, I'm okay with := as a distant third choice. (But see below.)
A ternary operator can't put *all* three clauses first. Only one can go first. And you know which one we picked? Not the condition, as C went with. Not the alternative "else" expression. But the primary "if" clause, in other words, the expression that we consider to be the usual case. So the ternary if supports my position, it isn't in opposition :-) But of course your general observation is correct. "Expression first" is violated by regular assignment, by long tradition. I believe that was inherited from mathematics, where is may or may not make sense, but either way it is acceptible (if only because of long familiarity!) for assignment statements. But we should reconsider it for expressions. Analogy: we write "with expression as name" for context managers. We could have put the name first and written "using name from expression", but that puts the name and expression in the wrong order. Similarly we don't write "import as np numpy", rather we use "import numpy as np". We put the entity being imported first, the name it is bound to last. But again, I acknowledge that there are exceptions, like for loops, and they've been around a long time. Back to the 1950s and the invention of Fortran. So no, this isn't an absolute showstopper.
I don't interact with the R community enough to know how commonly people use -> versus <- but you can certainly try it for yourself in the R interpreter if you have any doubts that it works. The statistician John Cook says -> is "uncommon": https://www.johndcook.com/blog/r_language_for_programmers/#assignment but in any case, I think that the idea of arrows as pointers, motion, "putting into" and by analogy assignment shouldn't be hard to grasp. The first time I saw pseudo-code using <- for assignment, I was confused by why the arrow was pointing to the left instead of the right but I had no trouble understanding that it implied taking the value at non-pointy end and moving it into the variable at the pointy end.
Well, it's your PEP, and I can't force you to treat my suggestion seriously, but I am serious about it and there have been a few other people agree with me. I grew up with Pascal and I like it, but it's 2018 and a good twenty to thirty years since Pascal was a mainstream language outside of academia. In 1988, even academia was slowly moving away from Pascal. I think the death knell of Pascal as a serious mainstream language was when Apple stopped using Pascal for their OS and swapped to C for System 7 in 1991. The younger generation of programmers today mostly know Pascal only as one of those old-timer languages that is "considered harmful", if even that. And there is an entire generation of kids growing up using CAS calculators for high school maths who will be familiar with -> as assignment. So in my opinion, while := is a fine third-choice, I really think that the arrow operator is better. -- Steve

On 2018-04-13 08:44, Steven D'Aprano wrote:
So my position is:
- "as" is more Pythonic and looks nicer;
Yes, perhaps if typing annotations had not chosen the colon but used a whitespace delimiter instead, adding a few more colons to the source would not be an issue. But in combination, it feels like there will be way too many colons in a potential future Python. One of the things I liked about it historically was its relative lack of punctuation and use of short words like in, and, not, as, etc. As mentioned before, I liked := for assignment in Pascal, but presumably since we are keeping == for testing, there's not as strong of an argument for that spelling. -Mike

On Sat, Apr 14, 2018 at 1:44 AM, Steven D'Aprano <steve@pearwood.info> wrote:
Which is also the most important clause in some situations, since it's the governing clause. In an if *statement*, it's the one in the header, before the indented block. I don't think there's enough logic here to draw a pattern from.
I don't know about professional-level mathematics, but certainly what I learned in grade school was conventionally written with the dependent variable before the equals sign. You'd write "y = x²" to graph a parabola, not "x² = y". So the programming language convention of putting the assignment target first was logical to me.
but in any case, I think that the idea of arrows as pointers, motion, "putting into" and by analogy assignment shouldn't be hard to grasp.
Agreed. Whichever way the arrow points, it's saying "the data flows thattaway". C++ took this to a cute level with "cout << blah", abusing the shift operators to signal data flow, and it's a nuisance because of operator precedence sometimes, but nobody can deny that it makes intuitive sense.
(Now I want to use "pointy end" somewhere in the PEP. Just because.)
If I were to take a poll of Python developers, offering just these three options: 1) TARGET := EXPR 2) EXPR as TARGET 3) EXPR -> TARGET and ask them to rank them in preferential order, I fully expect that all six arrangements would have passionate supporters. So far, I'm still seeing a strong parallel between expression-assignment and statement-assignment, to the point of definitely wanting to preserve that. ChrisA

On 11 April 2018 at 15:32, Chris Angelico <rosuav@gmail.com> wrote:
Thanks for putting this revised version together! You've already incorporated my feedback on semantics, so my comments below are mostly about the framing of the proposal in the context of the PEP itself.
Leading with these kinds of examples really doesn't help to sell the proposal, since they're hard to read, and don't offer much, if any, benefit over the status quo where assignments (and hence the order of operations) need to be spelled out as separate lines. Instead, I'd suggestion going with the kinds of examples that folks tend to bring up when requesting this capability: # Handle a matched regex if (match := pattern.search(data)) is not None: ... # A more explicit alternative to the 2-arg form of iter() invocation while (value := read_next_item()) is not None: ... # Share a subexpression between a comprehension filter clause and its output filtered_data = [y for x in data if (y := f(x)) is not None] All three of those examples share the common characteristic that there's no ambiguity about the order of operations, and the latter two aren't amenable to simply being split out into separate assignment statements due to the fact they're part of a loop. A good proposal should have readers nodding to themselves and thinking "I could see myself using that construct, and being happy about doing so", rather than going "Eugh, my eyes, what did I just read?" :)
"names" would also be eagerly bound here.
The example using the PEP syntax could be listed first in its own section, and then the others given as "These are the less obvious alternatives that this new capability aims to displace". Similar to my suggestion above, you may also want to consider making this example a filtered comprehension in order to show the proposal in its best light: results = [(x, y, x/y) for x in input_data if (y := f(x) )]
Similar to the comprehension section, I think this part could benefit from switching the order of presentation.
Frequently Raised Objections ============================
There needs to be a subsection here regarding the need to call `del` at class and module scope, just as there is for loop iteration variables at those scopes.
This argument will be strengthened by making the examples used in the PEP itself more attractive, as well as proposing suitable additions to PEP 8, such as: 1. If either assignment statements or assignment expressions can be used, prefer statements 2. If using assignment expressions would lead to ambiguity about execution order, restructure to use statements instead Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On 11 April 2018 at 16:22, Nick Coghlan <ncoghlan@gmail.com> wrote:
Agreed, this is a *much* better motivating example.
+1 on explicitly suggesting additions to PEP 8. Bonus points for PEP 8 additions that can be automatically checked by linters/style checkers (For example "avoid chained assignment expressions"). Paul

On Thu, Apr 12, 2018 at 1:22 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
Cool, thanks. I've snagged these (and your other examples) and basically tossed them into the PEP unchanged.
Yep, that was a clerical error on my part, now corrected.
Hmm, I'm not sure I follow. Are you saying that this is an objection to assignment expressions, or an objection to them not being statement-local? If the latter, it's really more about "rejected alternative proposals".
Fair enough. Also adding that chained assignment expressions should generally be avoided. Thanks for the recommendations! ChrisA

On 11 April 2018 at 22:28, Chris Angelico <rosuav@gmail.com> wrote:
Another one I think should be included (I'm a bit sad that it's not so obvious that no-one would ever even think of it, but the current discussion pretty much killed that hope for me). * Assignment expressions should never be used standalone - assignment statements should *always* be used in that case. That's also one that I'd like to see implemented as a warning in the common linters and style checkers. I'm still not convinced that this whole proposal is a good thing (the PEP 8 suggestions feel like fighting a rearguard action against something that's inevitable but ill-advised), but if it does get accepted it's in a lot better place now than it was when the discussions started - so thanks for all the work you've done on incorporating feedback. Paul

On 12 April 2018 at 07:28, Chris Angelico <rosuav@gmail.com> wrote:
It's both - accidentally polluting class and module namespaces is an argument against expression level assignments in general, and sublocal namespaces aimed to eliminate that downside. Since feedback on the earlier versions of the PEP has moved sublocal namespaces into the "rejected due to excessive conceptual complexity" box, that means accidental namespace pollution comes back as a downside that the PEP should mention. I don't think it needs to say much, just point out that they share the downside of regular for loops: if you use one at class or module scope, and don't want to export the name, you need to delete it explicitly. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Thu, Apr 12, 2018 at 07:28:28AM +1000, Chris Angelico wrote:
Fair enough. Also adding that chained assignment expressions should generally be avoided.
So long as its not a blanket prohibition, I'm good with that. Also, something like this: spam := 2*(eggs := expression) + 1 should not be considered a chained assignment. -- Steve

On Fri, Apr 13, 2018 at 9:17 PM, Steven D'Aprano <steve@pearwood.info> wrote:
Agreed. Chained assignment is setting the same value to two or more targets, without any further changes or anything: a = b = c = 0 Extremely handy as a statement (quickly setting a bunch of things to zero or None or something), but less useful with expression assignment. Incidentally, I've only just made "x := y := 1" legal tonight (literally during the writing of this post). Previously it needed parentheses, and I just didn't see much priority on fixing something that wasn't all that crucial :) ChrisA

Nick Coghlan writes:
My immediate take was "this syntax is too ugly to live", but so are Gila monsters, and I don't understand the virtues that lead Nick and Guido to take this thread seriously. So I will just leave that statement here. (no vote yet) More constructively, I found it amusing that the results were stuffed into generic one-character variables, while the temporaries got actual words, presumably standing in for mnemonic identifiers. Besides moving the examples, that should be fixed if these examples are to be used at all. I'm also with Paul (IIRC) who suggested formatting the second example on multiple lines. I suggest s/x/filling/ (sandwich) and s/y/omelet/. Steve

On 04/10/2018 10:32 PM, Chris Angelico wrote:
Title: Assignment Expressions
Thank you, Chris, for doing all this! --- Personally, I'm likely to only use this feature in `if` and `while` statements; if the syntax was easier to read inside longer expressions then I might use this elsewhere -- but as has been noted by others, the on-the-spot assignment creates asymmetries that further clutter the overall expression. As Paul noted, I don't think parenthesis should be mandatory if the parser itself does not require them. For myself, I prefer the EXPR as NAME variant for two reasons: - puts the calculation first, which is what we are used to seeing in if/while statements; and - matches already existing expression-level assignments (context managers, try/except blocks) +0.5 from me. -- ~Ethan~

On Thu, Apr 12, 2018 at 4:38 AM, Ethan Furman <ethan@stoneleaf.us> wrote:
Context managers and except blocks don't do the same thing though, so it's a false parallel. The 'as' keyword occurs in Python's grammar thus: 1) "with EXPR as target:" captures the return value from __enter__ 2) "except EXPR as NAME:" captures the exception value, not the type(s) 3) "import NAME as NAME" loads a module object given by a token (name) and captures that 4) "from NAME import NAME as NAME" captures an attribute of a module Three of them use a name, one allows arbitrary assignment targets. (You can "with spam as ham[0]:" but you can't "except Exception as ham[0]:".) Two of them have no expressions at all, so assignment expressions wouldn't logically be usable. The other two are *dangerous* false parallels, because "with foo as bar:" is semantically different from "with (foo as bar):"; it was so awkward to try to explain that away that I actually forbade any use of "(expr as name)" in the header of a with/except block. The parallel is not nearly as useful as it appears to be on first blush. The parallel with assignment statements is far closer; in fact, many situations will behave identically whether you use the colon or not.
- puts the calculation first, which is what we are used to seeing in if/while statements; and
Not sure what you mean here; if and while statements don't have anything BUT the calculation. A 'for' loop puts the targets before the evaluated expression.
- matches already existing expression-level assignments (context managers, try/except blocks)
They're not expression-level assignments though, or else I'm misunderstanding something here? ChrisA

On 04/11/2018 02:55 PM, Chris Angelico wrote:
On Thu, Apr 12, 2018 at 4:38 AM, Ethan Furman wrote:
Context managers and except blocks don't do the same thing though, so it's a false parallel.
They assign things to names using `as`. Close enough. ;)
If `with` is the first token on the line, then the context manager syntax should be enforced -- you get one or the other, not both.
Which is why we're using to seeing it first. ;)
A 'for' loop puts the targets before the evaluated expression.
Hmm, good point -- but it still uses a key word, `in`, to delineate between the two.
No, just me being sloppy with vocabulary. -- ~Ethan~

On Thu, Apr 12, 2018 at 9:03 AM, Ethan Furman <ethan@stoneleaf.us> wrote:
Okay, but because they don't assign the thing just before the word 'as', it's a dangerous parallel. Worse, a 'with' statement OFTEN assigns the thing just before the word 'as', making the parenthesized form frequently, but not always, the same as the unparenthesized.
That's what I had in the previous version: that 'as' bindings are straight-up not permitted in 'with' and 'except' headers. I wasn't able to implement that in the grammar, so I can't demo it for you, but the fact that this was a special case was *itself* a turn-off to some. Are special cases special enough to break the rules? Do we want something that is a subtle bug magnet?
Heh. You could just as easily say you're used to seeing it next to the colon :)
It does, yes. Fortunately we can't have a competing proposal for name bindings to be spelled "NAME in EXPR", because that's already a thing :D
Gotcha, no problem. For myself, I've been back and forth a bit about whether "as" or ":=" is the better option. Both of them have problems. Both of them create edge cases that could cause problems. Since the problems caused by ":=" are well known from other languages (and are less serious than they would be if "=" were the operator), I'm pushing that form. However, the 'as' syntax is a close contender (unlike most of the other contenders), so if someone comes up with a strong argument in its favour, I could switch. ChrisA

On Thu, Apr 12, 2018 at 10:44 AM, Ethan Furman <ethan@stoneleaf.us> wrote:
It can; and in fact, I have a branch where I had exactly that (with the SLNB functionality as well): https://github.com/Rosuav/cpython/tree/statement-local-variables But it creates enough edge cases that I was swayed by the pro-:= lobby. ChrisA

On Wed, Apr 11, 2018 at 03:32:04PM +1000, Chris Angelico wrote:
Have we really decided on spelling this as `target := expression`? You list this as a rejected spelling:
1. ``EXPR as NAME``, with or without parentheses::
stuff = [[f(x) as y, x/y] for x in range(5)]
but I don't think the objections given should be fatal:
The special casing you refer to would be to prohibit name binding expressions in "except" and "with" statements. You should explicitly say so in the PEP. I don't think that prohibiting those two forms is a big loss. I think any form of except (name := expression) as err: do_something(name) is going to be contrived. Likewise for `with` statements. I don't especially dislike := but I really think that putting the expression first is a BIG win for readability. If that requires parens to disambiguate it, so be it. You also missed the "arrow assignment operator" from various languages, including R: expression -> name (In an earlier post, I suggested R's other arrow operator, name <- expr, but of course that already evaluates as unary minus expr.) I think that there should be more attention paid to the idea of putting the expression first, rather than the name. -- Steve

On Fri, Apr 13, 2018 at 9:04 PM, Steven D'Aprano <steve@pearwood.info> wrote:
Parenthesis added to the rejection paragraph.
I agree as regards except statements. Not so much the with statements, though. How many times have people asked for "with (expr as name):" to be supported, allowing the statement to spread over multiple lines? With this syntax, it would suddenly be permitted - with dangerously similar semantics. For many MANY context managers, "with (expr as name):" would do the exact same thing as "with expr as name:". There is a general expectation that adding parentheses to an expression usually doesn't change the behaviour, and if it's legal, people will assume that the behaviour is the same. It isn't, and it's such a sneaky difference that I would call it a bug magnet. So if it's a bug magnet, what do we do? 1) Permit the subtly different semantics, and tell people to be careful 2) Forbid any use of "(expr as name)" in the header of a 'with' statement 3) Forbid it at top level, but permit it deeper down 4) Something else??
There's a mild parallel between "(expr as name)" and other uses of 'as', which bind to that name. Every other use of 'as' is part of a special syntactic form ('import', 'with', and 'except'), but they do all bind to that name. (Point of interest: You can "with expr as x[0]:" but none of the other forms allow anything other than a simple name.) There's a strong parallel between "target := value" and "target = value"; in fact, the section on the differences is incredibly short and could become shorter. The only really important difference is that augmented assignment is not supported (you can't say "x +:= 5"), partly because it'd mean creating a boatload of three-character operators for very little value, and partly because augmented-assignment-as-expression is hard to explain. Which is better? A weak parallel or a strong one? How important is putting the expression first? On balance, I'm currently in favour of the := syntax, but it's only a small difference.
I actually can't find anything about the -> operator, only the <- one. (Not that I looked very hard.) Is it a truly viable competitor, or just one that you'd like to see mentioned for completeness?
I think that there should be more attention paid to the idea of putting the expression first, rather than the name.
How many ways are there to bind a value to a name? Name last: * import x as y * from x import y as z * except x as y * with x as y Name first: * x = y * x += y # etc * for x in y * def x(.....) .... * def f(x=1) - arg defaults * class X: ... I'm seeing consistency here in that *EVERY* name binding where the name is at the end uses "as target" as its syntax. Everything else starts with the target, then defines what's being assigned to it. So I don't see much value in a "->" operator, except for the mere fact that it's different (and thus won't conflict in except/with); and the bulk of name bindings in Python put the name first. I don't have any really strong arguments in favour of :=, but I have a few weak ones and a few not quite so weak. So far, I have only weak arguments in favour of 'as'. ChrisA

On Fri, Apr 13, 2018 at 09:56:35PM +1000, Chris Angelico wrote:
I see your point, but why don't we support "with (expr as name):" to allow multiple lines? No, don't answer that... its off-topic. Forget I asked. In any case, we already allow similar syntax with different meaning in different places, for example, something that looks just like assignment inside expressions: functions = [len, ord, map, lambda x, y=1: x+y] But its not really an assignment as such, its a parameter declaration. If we agree that the benefit of putting the expression first is sufficiently large, or that the general Pythonic look of "expr as name" is sufficiently desirable (it just looks and reads nicely), then we can afford certain compromises. Namely, we can rule that: except expr as name: with expr as name: continue to have the same meaning that they have now and never mean assignment expressions. Adding parens should not change that. If you try to write something like: except (spam or eggs as cheese) or function(cheese) as name: with (spam or eggs as cheese) or function(cheese) as name: etc (or any other assignment expression, effectively anything which isn't currently allowed) then you get a syntax error. So this: with expr as name: process(name) will only work if expr returns an object with a context manager. But that's they way it works now, so nothing really changes. In other words, the rule is that "expr as name" keeps its current, older semantics in with and except statements, and NEVER means the new, PEP 572 assignment expression. Yes, that's a special case that breaks the rules, and I accept that it is a point against "as". But the Zen is a guideline, not a law of physics, and I think the benefits of "as" are sufficient that even losing a point it still wins.
Indeed. I wouldn't allow such a subtle difference in behaviour due to parens. That reminds me of the Python 1 and early 2.x except clauses, where except ValueError, TypeError: except (ValueError, TypeError): meant different things. I still shudder at that one.
So if it's a bug magnet, what do we do?
1) Permit the subtly different semantics, and tell people to be careful
No.
2) Forbid any use of "(expr as name)" in the header of a 'with' statement
You can't forbid it, because it is currently allowed syntax (albeit currently without the parens). So the rule is, it is allowed, but it means what it meant pre-PEP 572.
3) Forbid it at top level, but permit it deeper down
I don't know what that means. But whatever it means, probably no :-)
4) Something else??
Well, there's always the hypothetical -> arrow binding operator, or the Pascal := assignment operator (the current preference). I don't hate the := choice, I just think it is more Pascal-esque that Pythonic :-)
I disagree: I think it is a strong parallel. They're both name bindings. How much stronger do you want? True, we don't currently allow such things as import math as maths, mathematics, spam.modules[0] but we could if we wanted to and there was a sensible use-case for it.
There's a strong parallel between "target := value" and "target = value";
Sure. And for a statement, either form would be fine. I just think that in an expression, it is important enough to bring the expression to the front, even if it requires compromise elsewhere. [...]
Yes, as I mentioned in another post, R allows both -> and <-, some language called BETA uses ->, various calculator BASICs use -> (albeit with a special single character, not a digraph) as does HP RPN. Here's an example from R:
But whether it is viable or not depends on *us*, not what other languages do. No other language choose the syntax of ternary if expression before Python used it. We aren't limited to only using syntax some other language used first.
We shouldn't be choosing syntax because other syntax does the same. We should pick the syntax which is most readable and avoids the most problems. That's why Guido bucked the trends of half a century of programming languages, dozens of modern languages, and everything else in Python, to put the conditional in the middle of ternary if instead of the beginning or end. (And he was right to do so -- I like Python's ternary operator, even if other people think it is weird.) If people agree with me that it is important to put the expression first rather than the target name, then the fact that statements and for loops put the name first shouldn't matter. And if they don't, then I'm outvoted :-) -- Steve

Well this may be crazy sounding, but we could allow left or right assignment with name := expr expr =: name Although it would seem to violate the "only one obvious way" maxim, at least it avoids this overloaded meaning with the "as" of "except" and "with" On Fri, Apr 13, 2018 at 9:29 AM, Ethan Furman <ethan@stoneleaf.us> wrote:

On Fri, Apr 13, 2018 at 11:30 PM, Peter O'Connor <peter.ed.oconnor@gmail.com> wrote:
Hah. It took me multiple readings to even notice the change in the operator there, so I think this would cause a lot of confusion. (Don't forget, by the way, that an expression can be a simple name, and an assignment target can be more complicated than a single name, so it won't always be obvious on that basis.) It probably wouldn't *technically* conflict with anything, but it would get extremely confusing! ChrisA

On Apr 14 2018, Chris Angelico <rosuav-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org> wrote:
Well, if putting the expression first is generally considered better, the reasonable thing to do would be to allow *only* =:. -Best Nikolaus -- GPG Fingerprint: ED31 791B 2C5C 1613 AF38 8B8A D113 FCAC 3C4E 599F »Time flies like an arrow, fruit flies like a Banana.«

On 13 April 2018 at 14:18, Steven D'Aprano <steve@pearwood.info> wrote:
On Fri, Apr 13, 2018 at 09:56:35PM +1000, Chris Angelico wrote:
2) Forbid any use of "(expr as name)" in the header of a 'with' statement
You can't forbid it, because it is currently allowed syntax
It's not currently allowed:
(albeit currently without the parens).
Well, yes, but we're talking about *with* the parens.
So the rule is, it is allowed, but it means what it meant pre-PEP 572.
So it's a syntax error, because that's what it is pre-PEP 572. So it's allowed, but as a syntax error (which is what "not allowed" means". Huh? In any event it's a special case, because "with EXPR:" is valid, and "(12 as x)" is an example of an EXPR, but put these two together and you're saying you should get a syntax error. (Or if you're not, you haven't stated what you *are* proposing...) And there's no good justification for making this a special case (unless you argue in circles: it's worth being a special case because "as" is a good syntax for assignment expressions, and "as" is a good syntax because it's unambiguous...)
I agree that having the expression first is better. And I think that -> deserves consideration on that basis. I think "as" is *not* a good option for a "puts the expression first" option, because of the ambiguities that Chris has explained. But I also think that it's a relatively minor point, and I have bigger reservations than this about the PEP, so arguing over this level of detail isn't crucial to me. Paul

On Fri, Apr 13, 2018 at 02:30:24PM +0100, Paul Moore wrote:
It is without the parens, as I said: [...]
(albeit currently without the parens).
Well, yes, but we're talking about *with* the parens.
You might be, but I'm not. I'm talking about the conflict between: # pre-PEP 572 context manager with expr as name: # post-PEP 572, is it a context manager or an assignment expression? with expr as name: and I'm saying, don't worry about the syntactical ambiguity, just make a ruling that it is a context manager, never an assignment expression. 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.) [...]
I never said that "as" was unambiguous. I just spent a rather long post attempting to explain how to deal with that ambiguity. Sorry if I was not clear enough. I'm saying we can deal with it by simply defining what we want it to mean, and using the surrounding context to distinguish the cases. If it is in a with or except clause (just the header clause, not the following block) , then it means the same thing it means now. Everywhere else, it means assignment expression. Just like "x = 1" can mean assignment, or it can mean a parameter declaration. We don't have different syntax for those two distinct actions, we simply state that if "x = 1" is inside a lambda or function def, then it always means a parameter declaration and never an illegal assignment.
Indeed.
I think "as" is *not* a good option for a "puts the expression first" option, because of the ambiguities that Chris has explained.
I can see why people may dislike the "as" option. I think it comes down to personal taste. My aim is to set out what I see as a way around the with/except problem with a compromise: - allow "expr as name" anywhere; - require parentheses only to avoid syntactic ambiguity; - and simply ban assignment expressions in with/except clauses and keep the existing with/except semantics. I acknowledge that it isn't ideal, but compromises between conflicting requirements rarely are. If people don't agree with me, you're all wrong, er, that is to say, I understand your position even if I disagree *wink* -- Steve

So if I read this correctly, you're making an argument to ignore parens ? 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. Without parens -> old syntax + meaning with parens -> bind expr to name, because that's what the parens say. Ignoring parens, in any way, just seems like a bad idea. If you want to avoid with(expr as name):, shouldn't you make parens illegal ?

On Fri, Apr 13, 2018 at 05:04:00PM +0200, Jacco van Dorp wrote:
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.
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

On Sat, Apr 14, 2018 at 2:43 AM, Steven D'Aprano <steve@pearwood.info> wrote:
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").
The RHS of an assignment is an expression. But you can't move that first '(' any further to the left.
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).
Right.
Sure. And that's a good reason to straight-up *forbid* expression-as-name in a 'with' statement.
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.
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

2018-04-13 23:31 GMT+03:00 Chris Angelico <rosuav@gmail.com>:
Maybe it does not suit you, but what do you think about `SyntaxWarning` instead of `SyntaxError` for both `with` and `except`. By analogy how it was done for `global name` into function body prior to Python 3.6? With kind regards, -gdg

On Sat, Apr 14, 2018 at 7:33 AM, Kirill Balunov <kirillbalunov@gmail.com> wrote:
Warnings are often not seen. For an error this subtle, a warning wouldn't be enough. Good call though; that was one of the considerations, and if we knew for sure that warnings could be seen by the right people, they could be more useful for these cases. ChrisA

On Sat, Apr 14, 2018 at 12:50 AM, Steven D'Aprano <steve@pearwood.info> wrote:
Then we're talking about unrelated points, and "currently-allowed" is immaterial.
That part isn't at all in question. The meaning of "with expr as name:" MUST NOT change, because that would massively break backward compatibility.
For that to work, the assignment expression MUST be disallowed in the header of a 'with' statement. Which is one of the suggestions I made. In fact, it was the PEP's recommendation as of the last point when 'as' was the preferred syntax. I never got as far as implementing that, but it'd be the safest solution. And it's still a special case.
Definitely not; again, that would break backward compatibility. We are in agreement on that.
Then it HAS to be a SyntaxError. Because that's what it means now. Or are you saying "it means the same thing it would mean now if the parentheses were omitted"? Because that is definitely not an easy thing to explain.
If people don't agree with me, you're all wrong, er, that is to say, I understand your position even if I disagree *wink*
Funnily enough, that's my stance too. I guess we're all just wrong, wronger, and wrongest today :) ChrisA

On Fri, Apr 13, 2018 at 11:18 PM, Steven D'Aprano <steve@pearwood.info> wrote:
The answer is simple: for the same reason that you can't parenthesize *most* statements.
It's not an expression, so it doesn't follow the rules of expressions. There is special grammar around the import statement to permit this, and anywhere else, if it's not an expression, parens don't work. (Backslashes do, but that's because they function at a different level.)
That's a different sort of special case from what I had in mind, which is that illegal parenthesization should raise SyntaxError. Either way, though...
... yes, the false parallel is a strike against the "as" syntax.
Agreed.
It isn't currently-allowed syntax precisely because of those parens. So "what it meant pre-PEP 572" is "raise SyntaxError".
3) Forbid it at top level, but permit it deeper down
I don't know what that means. But whatever it means, probably no :-)
That would mean that this is a SyntaxError: with (get_file() as f): But this is allowed: with open(get_filename() as name): It's a more complicated rule, but a more narrow special case (the ONLY thing disallowed is the one thing that would be interpreted differently with and without parens), and there's no ambiguity here.
That's fair. And since Python has the "==" operator for comparison, using a Pascal-style ":=" for assignment isn't really paralleling properly. But purely within Python, any one of the three ('as', ':=', '->') is going to be at least somewhat viable, and it comes down to what they parallel *in Python*: 1) 'as' parallels 'import', 'with', and 'except', which perform name bindings based on some language magic using what's to the left of the 'as' 2) ':=' parallels '=', which takes what's on its right and assigns it to the target on the left 3) '->' parallels function return value annotations, but looks like data's moving from the left to the right.
True, they're name bindings. So are non-renaming import statements ("import os", "from pprint import pprint"), function and class definitions, and for loops. None of those use 'as'. So it's a weak parallel.
I'm not entirely sure what this would do, partly because I'm unsure whether it means to import "math" under the name "maths", and the other two separately, or to import "math" with three targets.
Why is it okay for a statement but not an expression? Genuine question, not scorning it. Particularly since expressions can be used as statements, so "value -> target" could be used as a statement.
Indeed, but for the sake of the PEP, it's useful to cite prior art. Just wanted to clarify.
Fair enough. That said, though, consistency DOES have value. The language fits into people's heads far better if parts of it behave the same way as other parts. The only question is, which consistencies matter the most? ChrisA

On 13 April 2018 at 23:18, Steven D'Aprano <steve@pearwood.info> wrote:
It's not completely off topic. as it's due to the fact we use "," to separate both context managers and items in a tuple, so "with (cm1, cm2, cm3):" is currently legal syntax that means something quite different from "with cm1, cm2, cm3:". While using the parenthesised form is *pointless* (since it will blow up at runtime due to tuples not being context managers), the fact it's syntactically valid makes us more hesitant to add the special case around parentheses handling than we were for import statements. The relevance to PEP 572 is as a reminder that since we *really* don't like to add yet more different cases to "What do parentheses indicate in Python?", we should probably show similar hesitation when it comes to giving ":" yet another meaning. This morning, I remembered a syntax idea I had a while back in relation to lambda expressions that could perhaps be better applied to assignment expressions, so I'll quickly recap the current options, and then go on to discussing that. Since the threads are pretty sprawling, I've also included a postscript going into more detail on my current view of the pros and cons of the various syntax proposals presented so far. Expression first proposals: while (read_next_item() as value) is not None: ... while (read_next_item() -> value) is not None: ... Target first proposal (current PEP): while (value := read_next_item()) is not None: ... New keyword based target first proposal: while (value from read_next_item()) is not None: ... The new one is a fairly arbitrary repurposing of the import system's "from" keyword, but it avoids all the ambiguities of "as", is easier to visually distinguish from other existing expression level keywords than "as", avoids giving ":" yet another meaning, still lets us use a keyword instead of a symbol, and gives the new expression type a more clearly self-evident name ("from expressions", as opposed to the "from statements" used for imports). It also more easily lends itself to skipping over the details of the defining expression when reading code aloud or in your head (e.g. "while value is not None, where value comes from read_next_item()" would be a legitimate way of reading the above for loop header, and you could drop the trailing clause completely when the details aren't particularly relevant). Avoiding the use of a colon as part of the syntax also means that if we wanted to, we could potentially allow optional type annotations in from-expressions ("target: annotation from expression"), and even adopt them as a shorthand for the sentinel pattern in function declarations (more on that below). As far as the connection with "from module import name" goes, given the proposed PEP 572 semantics, these three statements would all be equivalent: from dotted.module import name name = __import__("dotted_module", fromlist=["name"]).name name from __import__("dotted_module", fromlist=["name"]).name Other examples from the PEP: # Handle a matched regex if (match from pattern.search(data)) is not None: ... # Share a subexpression between a comprehension filter clause and its output filtered_data = [y for x in data if (y from f(x)) is not None] # Nested assignments assert 0 == (x from (y from (z from 0))) # Re-using fields in a container display stuff = [[y from f(x), x/y] for x in range(5)] And the set/dict examples display where ":=" could be visually confusing: # Set display with local name bindings data = { value_a from 1, value_b from 2, value_c from 3, } # Dict display with local key & value name bindings data = { key_a from 'a': value_a from 1, key_b from 'b': value_b from 2, key_c from 'c': value_c from 3, } Potential extension to simplifying the optional non-shared-mutable-default-argument pattern: # Shared mutable default (stored directly in f.__defaults__) def f(shared = []): .... # Unshared mutable default (implementation details TBD) def f(unshared from []): .... That last part would only be a potential extension beyond the scope of PEP 572 (since it would go against the grain of "name = expression" and "name from expression" otherwise being functionally equivalent in their behaviour), but it's an opportunity that wouldn't arise if a colon is part of the expression level name binding syntax. Cheers, Nick. P.S. The pros and cons of the current syntax proposals, as I see them: === Expression first, 'as' keyword === while (read_next_item() as value) is not None: ... Pros: * typically reads nicely as pseudocode * "as" is already associated with namebinding operations Cons: * syntactic ambiguity in with statement headers (major concern) * encourages a common misunderstanding of how with statements work (major concern) * visual similarity between "as" and "and" makes name bindings easy to miss * syntactic ambiguity in except clause headers theoretically exists, but is less of a concern due to the consistent type difference that makes the parenthesised form pointless === Expression first, '->' symbol === while (read_next_item() -> value) is not None: ... Pros: * avoids the syntactic ambiguity of "as" * "->" is used for name bindings in at least some other languages (but this is irrelevant to users for whom Python is their first, and perhaps only, programming language) Cons: * doesn't read like pseudocode (you need to interpret an arbitrary non-arithmetic symbol) * invites the question "Why doesn't this use the 'as' keyword?" * symbols are typically harder to look up than keywords * symbols don't lend themselves to easy mnemonics * somewhat arbitrary repurposing of "->" compared to its use in function annotations === Target first, ':=' symbol === while (value := read_next_item()) is not None: ... Pros: * avoids the syntactic ambiguity of "as" * being target first provides an obvious distinction from the "as" keyword * ":=" is used for name bindings in at least some other languages (but this is irrelevant to users for whom Python is their first, and perhaps only, language) Cons: * symbols are typically harder to look up than keywords * symbols don't lend themselves to easy mnemonics * subject to a visual "line noise" phenomenon when combined with other uses of ":" as a syntactic marker (e.g. slices, dict key/value pairs, lambda expressions, type annotations) === Target first, 'from' keyword === while (value from read_next_item()) is not None: # New ... Pros: * avoids the syntactic ambiguity of "as" * being target first provides an obvious distinction from the "as" keyword * typically reads nicely as pseudocode * "from" is already associated with a namebinding operation ("from module import name") Cons: * I'm sure we'll think of some more, but all I have so far is that the association with name binding is relatively weak and would need to be learned -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Sat, Apr 14, 2018 at 11:54 PM, Chris Angelico <rosuav@gmail.com> wrote:
To me, "from" strongly suggests that an element is being obtained from a container/collection of elements. This is how I conceptualize "from module import name": "name" refers to an object INSIDE the module, not the module itself. If I saw if (match from pattern.search(data)) is not None: ... I would guess that it is equivalent to m = next(pattern.search(data)) if m is not None: ... i.e. that the target is bound to the next item from an iterable (the "container"). Cheers, Nathan

On 15 April 2018 at 13:54, Chris Angelico <rosuav@gmail.com> wrote:
Ah, I forgot about that usage. The keyword usage is at least somewhat consistent, in that it's short for: _tmp = exc _exc.__cause__ from otherexc raise exc However, someone writing "raise (ExcType from otherexc)" could be confusing, since it would end up re-raising "otherexc" instead of wrapping it in a new ExcType instance. If "otherexc" was also an ExcType instance, that would be a *really* subtle bug to try and catch, so this would likely need the same kind of special casing as was proposed for "as" (i.e. prohibiting the top level parentheses). I also agree with Nathan that if you hadn't encountered from expressions before, it would be reasonable to assume they were semantically comparable to "target = next(expr)" rather than just "target = expr". Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

2018-04-15 6:08 GMT+03:00 Nick Coghlan <ncoghlan@gmail.com>:
Despite the fact that "with (cm1,cm2, cm3):" currently is the legal syntax, but as you said and as it was also noted before in this thread - it is "pointless" in 99% cases (in context of with statement) and will fail at runtime. Therefore, regardless of this PEP, maybe it is fair to make it at least to be a `SyntaxWarning` (or `SyntaxError`)? Better fail sooner than later. we should probably
show similar hesitation when it comes to giving ":" yet another meaning.
Yes, `:` is used (as a symbol) in a lot of places (in fact there is not much in it), but in some places Python looks as a combination of words and colons.
I understand that this list is subjective. But as for me it will be huge PRO that the expression comes first.
In reality, the first two points can be explained (if it will be required at all). Misunderstanding is a consequence of a lack of experience. I don't understand the the point about "visual similarity between "as" and "and" can you elaborate on this point a little bit more?
The same as previous, the expression comes first is a huge PRO for me and I'm sure for many others too. With the second point I agree that it is somewhat irrelevant.
Here I am a bit disagree with you. The most common for of assignment in formal pseudo-code is `name <- expr`. The second most common form, to my regret, is - `:=`. The `<-` form is not possible in Python and that is why `expr -> name` was suggested.
* invites the question "Why doesn't this use the 'as' keyword?"
All forms invites this question :)))
For me it is a CON. Originally the rationale of this PEP was to reduce the number of unnecessary calculations and to provide a useful syntax to make a name binding in appropriate places. It should not, in any way, replace the existing `=` usual way to make a name binding. Therefore, as I see it, it is one of design goals to make the syntax forms of `assignment statement` and `assignment expression` to be distinct and `:=` does not help with this. This does not mean that this new syntax form should not be convenient, but it should be different from the usual `=` form.
Totally agree with the last point!
As above.
* typically reads nicely as pseudocode
As for me this form implies _extraction_+binding (finding inside + binding) instead of just binding.
* "from" is already associated with a namebinding operation ("from module import name")
but module is a namespace, and `from` means _extract_ and bind.
With kind regards, -gdg

On Sun, Apr 15, 2018 at 12:19 PM, Kirill Balunov <kirillbalunov@gmail.com> wrote:
2018-04-15 6:08 GMT+03:00 Nick Coghlan <ncoghlan@gmail.com>:
[...]
Exactly, all forms invites this and other questions. First of all, coming back to original spelling choice arguments [Sorry in advance if I've missed some points in this huge thread] citation from PEP: "Differences from regular assignment statements" [...] "Otherwise, the semantics of assignment are unchanged by this proposal." So basically it's the same Python assignment? Then obvious solution seems just to propose "=". But I see Chris have put this in FAQ section: "The syntactic similarity between ``if (x == y)`` and ``if (x = y)`` ...." So IIUC, the *only* reason is to avoid '==' ad '=' similarity? If so, then it does not sound convincing at all. Of course Python does me a favor showing an error, when I make a typo like this: if (x = y) But still, if this is the only real reason, it is not convincing. Syntactically seen, I feel strong that normal '=' would be the way to go. Just look at this: y = ((eggs := spam()), (cheese := eggs.method()) y = ((eggs = spam()), (cheese = eggs.method()) The latter is so much cleaner, and already so common to any old or new Python user. And does not raise a question what this ":=" should really mean. (Or probably it should raise such question?) Given the fact that the PEP gives quite edge-case usage examples only, this should be really more convincing. And as a side note: I personally find the look of ":=" a bit 'noisy'. Another point: *Target first vs Expression first* ======================= Well, this is nice indeed. Don't you find that first of all it must be decided what should be the *overall tendency for Python*? Now we have common "x = a + b" everywhere. Then there are comprehensions (somewhat mixed direction) and "foo as bar" things. But wait, is the tendency to "give the freedom"? Then you should introduce something like "<--" in the first place so that we can write normal assignment in both directions. Or is the tendency to convert Python to the "expression first" generally? So if this question can be answered first, then I think it will be more constructive to discuss the choice of particular spellings. Mikhail

On 15 April 2018 at 19:41, Mikhail V <mikhailwas@gmail.com> wrote:
It's thoroughly convincing, because we're already familiar with the consequences of folks confusing "=" and "==" when writing C & C++ code. It's an eternal bug magnet, so it's not a design we're ever going to port over to Python. (And that's even before we get into the parsing ambiguity problems that attempting to reuse "=" for this purpose in Python would introduce, especially when it comes to keyword arguments). The only way Python will ever gain expression level name binding support is with a spelling *other than* "=", as when that's the proposed spelling, the answer will be an unequivocal "No, we're not adding that". Even if the current discussion does come up with a potentially plausible spelling, the final determination on python-dev may *still* be "No, we're not going to add that". That isn't a predetermined answer though - it will depend on whether or not a proposal can be developed that threads the small gap between "this adds too much new cognitive overhead to reading and writing the language" and "while this does add more complexity to the base language, it provides sufficient compensation in allowing common ideas to be expressed more simply".
Consider how close the second syntax is to "y = f(eggs=spam(), cheese=fromage())", though.
Given the fact that the PEP gives quite edge-case usage examples only, this should be really more convincing.
The examples in the PEP have been updated to better reflect some of the key motivating use cases (embedded assignments in if and while statement conditions, generator expressions, and container comprehensions)
And as a side note: I personally find the look of ":=" a bit 'noisy'.
You're not alone in that, which is one of the reasons finding a keyword based option that's less syntactically ambiguous than "as" could be an attractive alternative.
There's no general tendency towards expression first syntax, nor towards offering flexibility in whether ordinary assignments are target first. All the current cases where we use the "something as target" form are *not* direct equivalents to "target = something": * "import dotted.modname as name": also prevents "dotted" getting bound in the current scope the way it normally would * "from dotted import modname as name": also prevents "modname" getting bound in the current scope the way it normally would * "except exc_filter as exc": binds the caught exception, not the exception filter * "with cm as name": binds the result of __enter__ (which may be self), not the cm directly Indeed, https://www.python.org/dev/peps/pep-0343/#motivation-and-summary points out that it's this "not an ordinary assignment" aspect that lead to with statements using the "with cm as name:" structure in the first place - the original proposal in PEP 310 was for "with name = cm:" and ordinary assignment semantics. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Sun, Apr 15, 2018 at 2:01 PM, Nick Coghlan <ncoghlan@gmail.com> wrote:
Im personally "0" on the whole proposal. Just was curious about that "demonisation" of "=" and "==" visual similarity. Granted, writing ":=" instead of "=" helps a little bit. But if the ":=" will be accepted, then we end up with two spellings :-)
Keyword variants look less appealing than ":=". but if it had to be a keyword, then I'd definitely stay by "TARGET keyword EXPR" just not to swap the traditional order. Mikhail

2018-04-15 12:41 GMT+03:00 Mikhail V <mikhailwas@gmail.com>:
[OT] To be honest I never liked the fact that `=` was used in various programming languages as assignment. But it became so common that and I got used to it and even stopped taking a sedative :) So IIUC, the *only* reason is to avoid '==' ad '=' similarity?
You are not alone. On the other hand it is one of the strengths of Python - not allow to do so common and complex to finding bugs. For me personally, `: =` looks and feels just like normal assignment statement which can be used interchangeable but in many more places in the code. And if the main goal of the PEP was to offer this `assignment expression` as a future replacement for `assignment statement` the `:=` syntax form would be the very reasonable proposal (of course in this case there will be a lot more other questions). But somehow this PEP does not mean it! And with the current rationale of this PEP it's a huge CON for me that `=` and `:=` feel and look the same.
As it was noted previously `<-` would not work because of unary minus on the right:
If the idea of the whole PEP was to replace `assignment statement` with `assignment expression` I would choose name first. If the idea was to offer an expression with the name-binding side effect, which can be used in the appropriate places I would choose expression first. With kind regards, -gdg

On Sun, Apr 15, 2018 at 4:05 AM, Kirill Balunov <kirillbalunov@gmail.com> wrote:
I haven't kept up with what's in the PEP (or much of this thread), but this is the key reason I strongly prefer := as inline assignment operator.
But somehow this PEP does not mean it! And with the current rationale of this PEP it's a huge CON for me that `=` and `:=` feel and look the same.
Then maybe the PEP needs to be updated. -- --Guido van Rossum (python.org/~guido)

On Mon, Apr 16, 2018 at 3:19 AM, Guido van Rossum <guido@python.org> wrote:
I can never be sure what people are reading when they say "current" with PEPs like this. The text gets updated fairly frequently. As of time of posting, here's the rationale: ----- Naming the result of an expression is an important part of programming, allowing a descriptive name to be used in place of a longer expression, and permitting reuse. Currently, this feature is available only in statement form, making it unavailable in list comprehensions and other expression contexts. Merely introducing a way to assign as an expression would create bizarre edge cases around comprehensions, though, and to avoid the worst of the confusions, we change the definition of comprehensions, causing some edge cases to be interpreted differently, but maintaining the existing behaviour in the majority of situations. ----- Kirill, is this what you read, and if so, how does that make ':=' a negative? The rationale says "hey, see this really good thing you can do as a statement? Let's do it as an expression too", so the parallel should be a good thing. ChrisA

[Guido] 2018-04-15 20:19 GMT+03:00 Guido van Rossum <guido@python.org>:
[Chris] 2018-04-15 23:28 GMT+03:00 Chris Angelico <rosuav@gmail.com>:
Yes, this is what I read. I understand why you have such a question so I'll try to explain my position in more detail. Also I want to add that I did not fully understand about which part Guido said - "Then maybe the PEP needs to be updated." Therefore, I allow myself to assume that he had in mind the following - "The assignment expression should be semantically equivalent to assignment statement and perceived as a theoretically possible future replacement (usage) of assignment statement." If this is really the case and I understood correctly - I will repeat that for me the current state of the PEP does not fully imply this. 1. Part - "Then maybe the PEP needs to be updated." If you really see it as a theoretical substitute for assignment statement in future Python. I will update the rationale with maybe the following (I immediately warn you that I do not pretend to have a good English style): ----- Naming the result of an expression is an important part of programming, allowing a descriptive name to be used in place of a longer expression, and permitting reuse. Currently, in Python this feature is available only in statement form, making it unavailable in list comprehensions and other expression contexts. This restriction, of making it as a statement, was done primarily to avoid the usual trap of `=` vs `==` in C/C++ language. Despite this, it is evident that the ability to assign a name within an expression is convenient, allows to avoid redundant recalculations of the same expression and is a familiar feature from other programming languages. Thus the main aim of this PEP is to provide a syntax which will allow to assign as an expression and be semantically and visually interchangeable with the assignment statement. ... ----- In this case, I do not see any reason to discuss the alternative syntax - there is really only one choice `:=`. And then for me the list of open questions would look like (for example): 1. ...Is it worth accepting? 2. ...Should the other forms (+=, *=, basically all) that can not be confused with `==` be changed to expressions? ... 2. Part - How do I understand the current state of the PEP I perceive the current rationale as "hey, see this really good thing you can do as a statement? Let's do it as an expression too". Which for me means opportunities to discuss the following questions: 1. Should assignment expression be viewed as a replacement of an assignment statement or as a complement to it? 2. Which spelling should have an assignment expression? ( depends on the previous ) 3. Does it make sense to make it valid only in certain context or in any? ... and many others ... You are the author of this PEP. Therefore, you choose how you take it, how you feel it and what kind of feedback you are willing to accept, and in any case I will respect your choice :) with kind regards, -gdg

On Sun, Apr 15, 2018 at 7:19 PM, Kirill Balunov <kirillbalunov@gmail.com> wrote:
I don't think we're ever going to unify everyone on an arbitrary question of "expression first" or "name first". But to all the "expression first" people, a question: what if the target is not just a simple name? while (read_next_item() -> items[i + 1 -> i]) is not None: print("%d/%d..." % (i, len(items)), end="\r") Does this make sense? With the target coming first, it perfectly parallels the existing form of assignment:
The unpacking syntax is a bit messy, but with expression assignment, we can do this:
Okay, it's not quite as simple as C's "items[i++]" (since you have to start i off at negative one so you can pre-increment), but it's still logical and sane. Are you as happy with that sort of complex expression coming after 'as' or '->'? Not a rhetorical question. I'm genuinely curious as to whether people are expecting "expression -> NAME" or "expression -> TARGET", where TARGET can be any valid assignment target. ChrisA

2018-04-15 15:21 GMT+03:00 Chris Angelico <rosuav@gmail.com>:
I completely agree with you that it is impossible to unify everyone opinion - we all have different background. But this example is more likely to play against this PEP. This is an extra complexity within one line and it can fail hard in at least three obvious places :) And I am against this usage no matter `name first` or `expression first`. But i will reask this with following snippets. What do you choose from this examples: 0. while (items[i := i+1] := read_next_item()) is not None: print(r'%d/%d' % (i, len(items)), end='\r') 1. while (read_next_item() -> items[(i+1) -> i]) is not None: print(r'%d/%d' % (i, len(items)), end='\r') 2. while (item := read_next_item()) is not None: items[i := (i+1)] = item print(r'%d/%d' % (i, len(items)), end='\r') 3. while (read_next_item() -> item) is not None: items[(i+1) -> i] = item print(r'%d/%d' % (i, len(items)), end='\r') 4. while (item := read_next_item()) is not None: i = i+1 items[i] = item print(r'%d/%d' % (i, len(items)), end='\r') 5. while (read_next_item() -> item) is not None: i = i+1 items[i] = item print(r'%d/%d' % (i, len(items)), end='\r') I am definitely Ok with both 2 and 3 here. But as it was noted `:=` produces additional noise in other places and I am also an `expression first` guy :) So I still prefer variant 3 to 2. But to be completely honest, I would write it in the following way: for item in iter(read_next_item, None): items.append(item) print(r'%d/%d' % (i, len(items)), end='\r') With kind regards, -gdg

2018-04-15 17:17 GMT+03:00 Kirill Balunov <kirillbalunov@gmail.com>:
Oh, I forgot about `i`: for item in iter(read_next_item, None): i += 1 items.append(item) print(r'%d/%d' % (i, len(items)), end='\r') With kind regards, -gdg

On Mon, Apr 16, 2018 at 12:17 AM, Kirill Balunov <kirillbalunov@gmail.com> wrote:
These two are matching what I wrote, and are thus the two forms under consideration. I notice that you added parentheses to the second one; is there a clarity problem here and you're unsure whether "i + 1 -> i" would capture "i + 1" or "1"? If so, that's a downside to the proposal.
All of these are fundamentally different from what I'm asking: they are NOT all expressions that can be used in the while header. So it doesn't answer the question of "expression first" or "target first". Once the expression gets broken out like this, you're right back to using "expression -> NAME" or "NAME := expression", and it's the same sort of simple example that people have been discussing all along.
And that's semantically different in several ways. Not exactly a fair comparison. I invite you to write up a better example with a complex target. ChrisA

2018-04-15 23:22 GMT+03:00 Chris Angelico <rosuav@gmail.com>:
Yes parentheses were used only for clarity. I agree that I misunderstood the purpose of your question. I have no problem if the right part is a complex target, but maybe my perception is biased. With kind regards, -gdg

On Sun, Apr 15, 2018 at 10:21:02PM +1000, Chris Angelico wrote:
I don't see why it would make a difference. It doesn't to me.
Does this make sense? With the target coming first, it perfectly parallels the existing form of assignment:
Yes, except this isn't ordinary assignment-as-a-statement. I've been mulling over the question why I think the expression needs to come first here, whereas I'm satisfied with the target coming first for assignment statements, and I think I've finally got the words to explain it. It is not just long familiarity with maths and languages that put the variable first (although that's also part of it). It has to do with what we're looking for when we read code, specifically what is the primary piece of information we're initially looking for. In assignment STATEMENTS the primary piece of information is the target. Yes, of course the value assigned to the target is important, but often we don't care what the value is, at least not at first. We're hunting for a known target, and only when we find it do we care about the value it gets. A typical scenario: I'm reading a function, and I scan down the block looking at the start of each line until I find the variable I want: spam = don't care eggs = don't care self.method(don't care) cheese = ... <<<==== HERE IT IS so it actually helps to have the name up front. Copying standard maths notation for assignment (variable first, value second) is a good thing for statements. With assignment-statements, if you're scanning the code for a variable name, you're necessarily interested in the name and it will be helpful to have it on the left. But with assignment-expressions, there's an additional circumstance: sometimes you don't care about the name, you only care what the value is. (I expect this will be more common.) The name is just something to skip over when you're scanning the code looking for the value. # what did I pass as the fifth argument to the function? result = some_func(don't care, spam := don't care, eggs := don't care, self.method(don't care), cheese := HERE IT IS, ...) Of course it's hard counting commas so it's probably better to add a bit of structure to your function call: result = some_func(don't care, spam := don't care, eggs := don't care, self.method(don't care), cheese := HERE IT IS, ...) But this time we don't care about the name. Its the value we care about: result = some_func(don't care, don't care -> don't care don't care -> don't care don't care(don't care), HERE IT IS .... , ...) The target is just one more thing you have to ignore, and it is helpful to have expression first and the target second. Some more examples: # what am I adding to the total? total += don't care := expression # what key am I looking up? print(mapping[don't care := key]) # how many items did I just skip? self.skip(don't care := obj.start + extra) versus total += expression -> don't care print(mapping[key -> don't care]) self.skip(obj.start + extra -> don't care) It is appropriate for assignment statements and expressions to be written differently because they are used differently. [...]
I don't know why you would write that instead of: items = [None]*10 for i in range(3): items[i] = input("> ") or even for that matter: items = [input("> ") for i in range(3)] + [None]*7 but whatever floats your boat. (Python isn't just not Java. It's also not C *wink*)
Are you as happy with that sort of complex expression coming after 'as' or '->'?
Sure. Ignoring the output of the calls to input(): items = [None] * 10 i = -1 items[i + 1 -> i] = input("> ") items[i + 1 -> i] = input("> ") items[i + 1 -> i] = input("> ") which isn't really such a complex target. How about this instead? obj = SimpleNamespace(spam=None, eggs=None, aardvark={'key': [None, None, -1]} ) items[obj.aardvark['key'][2] + 1 -> obj.aardvark['key'][2]] = input("> ") versus: items[obj.aardvark['key'][2] := obj.aardvark['key'][2] + 1] = input("> ") Neither is exactly a shining exemplar of great code designed for readability. But putting the target on the right doesn't make it worse. -- Steve

2018-04-15 18:58 GMT+03:00 Steven D'Aprano <steve@pearwood.info>:
This made my day! :) The programming style when you absolutely don't care :))) I understand that this is a typo but it turned out to be very funny. In general, I agree with everything you've said. And I think you found a very correct way to explain why expression should go first in assignment expression. With kind regards, -gdg

On Mon, Apr 16, 2018 at 1:58 AM, Steven D'Aprano <steve@pearwood.info> wrote:
Okay, that's good. I just hear people saying "name" a lot, but that would imply restricting the grammar to just a name, and I don't know how comfortable people are with more complex targets.
It is appropriate for assignment statements and expressions to be written differently because they are used differently.
I don't know that assignment expressions are inherently going to be used in ways where you ignore the assignment part and care only about the expression part. And I disagree that assignment statements are used primarily the way you say. Frequently I skim down a column of assignments, caring primarily about the functions being called, and looking at the part before the equals sign only when I come across a parameter in another call; the important part of the line is what it's doing, not where it's stashing its result.
You and Kirill have both fallen into the trap of taking the example too far. By completely rewriting it, you destroy its value as an example. Write me a better example of a complex target if you like, but the question is about how you feel about complex assignment targets, NOT how you go about creating a particular list in memory. That part is utterly irrelevant.
The calls to input were in a while loop's header for a reason. Ignoring them is ignoring the point of assignment expressions. ChrisA

On Mon, Apr 16, 2018 at 06:16:46AM +1000, Chris Angelico wrote: [...]
Chris, I must admit that I'm utterly perplexed at this. Your example is as far as from a complex assignment target as you can possibly get. It's a simple name! i := i + 1 The target is just "i", a name. The point I was making is that your example is not a good showcase for this suggested functionality. Your code violates DRY, repeating the exact same line three times. It ought to be put in a loop, and once put in a loop, the justification for needing assignment-expression disappears. But having said that, I did respond to your question and swapping the order around: items[i + 1 -> i] = input("> ") It's still not a "complex target", the target is still just a plain ol' name, but it is precisely equivalent to your example. And then I went further and re-wrote your example to use a genuinely complex target, which I won't repeat here.
What while loop? Your example has no while loop. But regardless, we don't need to care about the *output* (i.e. your keypresses echoed to stdout) when looking at the code sample. -- Steve

On Tue, Apr 17, 2018 at 1:54 AM, Steven D'Aprano <steve@pearwood.info> wrote:
Thanks so much for the aggressively-trimmed quote. For once, though, TOO aggressive. What you're focusing on is the *unrolled* version of a two-line loop. Look at the actual loop, please, and respond to the actual question. :|
Not after it got trimmed, no. Here's what I actually said in my original post: while (read_next_item() -> items[i + 1 -> i]) is not None: print("%d/%d..." % (i, len(items)), end="\r") Now, if THAT is your assignment target, are you still as happy as you had been, or are you assuming that the target is a simple name? ChrisA

On Tue, Apr 17, 2018 at 03:36:34AM +1000, Chris Angelico wrote: [further aggressive snippage] *wink*
Ah, I never even picked up on the idea that the previous while loop was connected to the following bunch of calls to input(). The code didn't seem to be related: the first was already using -> syntax and did not use input(), the second used := syntax and did.
I'll give the answer I would have given before I read Nick's comments over on Python-Dev: sure, I'm happy, and no, I'm not assuming the target is a simple name. But having seen Nick's response on Python-Dev, I'm now wondering whether this should be limited to simple names. Further discussion in reply to Nick's post over on Python-Dev please. -- Steve

On 2018-04-15 08:58, Steven D'Aprano wrote:
Interesting. I think your arguments are pretty reasonable overall. But, for me, they just don't outweigh the fact that "->" is an ugly assignment operator that looks nothing like the existing one, whereas ":=" is a less-ugly one that has the additional benefit of looking like the existing one. From your arguments I am convinced that putting the expression first has some advantages, but they just don't seem as important to me as they apparently do to you. -- Brendan Barnwell "Do not follow where the path may lead. Go, instead, where there is no path, and leave a trail." --author unknown

On Sun, Apr 15, 2018 at 6:58 PM, Steven D'Aprano <steve@pearwood.info> wrote:
... [SNIP] ....
It is appropriate for assignment statements and expressions to be written differently because they are used differently.
Wow. That feeling when you see someone giving reasonable arguments but in the end comes up with such doubtful conclusions. So you agree that in general you may need to spot values and in other case function calls or expressions. And that's it, its just depends. So if you swap the order and in some _single_ particular case you may notice tiny advantage, you conclude that the whole case with expression assignment needs this order. Lets just return to some of proposed examples (I use "=" in both examples to be less biased here): 1. if ( match = re.match("foo", S) ) == True: print("match:", match) 2. if ( re.match("foo", S) = match ) == True: print("match:", match) Now seriously, you may argue around those "pronounce" theoretical bla bla, like "take the result and save it in a token". But the variant 1. is just better, because _it is what it is in Python_. So it is better not because it is better looking or whatever, it is same sh** turned around. So just don't turn it around! Here the 1st variant can be unwrapped to: match = re.match("foo", S) if match == True: print("match:", match) Do you see what I mean? When I read code I don't have all those things you describe in a millisecond : - look at the pointy end of operator - think, oh this shows to the right - seems like I save the value there - yep, that's the way I imply things to work - stroking the belly .... Instead I just parse visually some smaller parts of code and it's just better if the assignment is in the same order as everywhere. Yes, in some single case one order can look better, but in this case it's just not good to mix those. Mikhail

On Mon, Apr 16, 2018 at 11:05 PM, Mikhail V <mikhailwas@gmail.com> wrote:
You start by attempting to be less biased, but you're using existing Python syntax and then justifying one of the two options because it's existing Python syntax. I'm not sure that that's a strong argument :)
So it is better not because it is better looking or whatever, it is same sh** turned around. So just don't turn it around!
Obviously if the chosen token is ":=", it's going to be target first.
Here are the three most popular syntax options, and how each would be explained: 1) "target := expr" ==> It's exactly the same as other forms of assignment, only now it's an expression. 2) "expr as name" ==> It's exactly the same as other uses of "as", only now it's just grabbing the preceding expression, not actually doing anything with it 3) "expr -> name" ==> The information went data way. So either you take a parallel from elsewhere in Python syntax, or you take a hopefully-intuitive dataflow mnemonic symbol. Take your pick. ChrisA

On 4/16/18 1:42 PM, Chris Angelico wrote:
My problem with the "->" option is that function annotations already use "->" to indicate the return type of a function. This is an unfortunate parallel from elsewhere in Python syntax, since the meaning is completely different. ":=" is at least new syntax. "as" is nice in that it's already used for assignment, but seems to be causing too much difficulty in parsing, whether by compilers or people. --Ned.

On Mon, Apr 16, 2018 at 11:11 AM Ned Batchelder <ned@nedbatchelder.com> wrote:
FWIW - We used "as" in our Python C++ binding interface description language in CLIF to denote renaming from the original C++ name to a new name in Python - effectively an assignment syntax. https://github.com/google/clif/blob/master/clif/python/primer.md I currently have a "-0" opinion on the entire PEP 572 as I don't buy that assignments within expressions are even a good thing to have in the language. #complexity - Think of people learning the language. -gps

How about "name being expression" - this avoids the already used "as" while being searchable, reasonably short and gives a reasonably clear, (at least to English speakers), indication of what is going on. It can also be typed on an ASCII keyboard without having to have a helper program or memorising Unicode codes and can be displayed or printed without having to install specialised fonts. If a postfix notation is considered desirable, either instead or as well as "being", then possibly another synonym would suit such as "expression stored_as name" or "expression storedas name" (not apologies for the awkward name as I personally find it an awkward construction just like Reverse Polish). -- Steve (Gadget) Barnes Any opinions in this message are my personal opinions and do not reflect those of my employer. --- This email has been checked for viruses by AVG. http://www.avg.com

On Tue, Apr 17, 2018 at 5:11 AM, Steve Barnes <gadgetsteve@live.co.uk> wrote:
IMO searchability isn't enough of an advantage to justify creating a new keyword, which could potentially break people's code. (I don't think it'll break the stdlib, but it'll almost certainly break at least some code out there.) New keywords have an extremely high bar to reach. ChrisA

On Mon, Apr 16, 2018 at 8:42 PM, Chris Angelico <rosuav@gmail.com> wrote:
As I initially said, I just don't find this choice list fair. and it should be: 1) "target = expr" (which would be first intuitive idea from user's PoV, and if can't be made then explained to Python people why not) 2) "target := expr" (as probably best close alternative) 3) "target ? expr" (where ? is some other word/character - IIRC "target from expr" was proposed once) That's it. But well, if I compare to your choice list - there is ":=" option you have as well :)

On Tue, Apr 17, 2018 at 5:42 AM, Mikhail V <mikhailwas@gmail.com> wrote:
That one is dealt with in the PEP. It is not an option.
2) "target := expr" (as probably best close alternative)
Yep, got that one.
3) "target ? expr" (where ? is some other word/character - IIRC "target from expr" was proposed once)
... which isn't specific enough to be a front-runner option.
Yes. I'm not sure what's unfair about the options I've given there. ChrisA

[Chris]
It does give a natural solution to one of the problematic examples, because as a very-low-precedence operator it would suck up "everything to the left" instead of "everything to the right" as "the expression part":: if f(x) -> a is not None: can't be misunderstood, but: if a := f(x) is not None: is routinely misunderstood. On the other hand, if assignment expressions are expanded to support all the forms of unpacking syntax (as Guido appears to favor), then other cases arise: if f() -> a, b > (3, 6): Is that: if ((f() -> a), b) > (3, 6): or if (f() -> (a, b)) > (3, 6): ? Which is an argument in favor of ":=" to me: an assignment statement can be pretty complex, and if an assignment operator can become just as complex then it's best if it looks and works (as much as possible} exactly like an assignment statement. If someone writes if a, b := f() > (3, 6): it's easy to explain why that's broken by referring to the assignment statement a, b = f() > (3, 6) "You're trying to unpack a Boolean into a 2-tuple - add parens to express what you really want."

Before, I briefly mentioned the idea if this could be unified with except/with's "as". To the casual observer, they're really similar. However, their semantics would be totally different, and people don't seem to like a unification attempt. A huge argument against "as" would be to prevent confusion, especially for new people. I must admit I like putting the expression first, though. Even if it's just to make it harder to mix it up with normal assignment. Perhaps => could be used - it's a new token, unlike -> which is used to annotate return values, it's not legal syntax now(so no backwards compatibility issues), and used a for similar purposes in for example php when declaring associative arrays.($arr = array("key"=>"value");). I'm not convinced myself, though.

On 11 April 2018 at 06:32, Chris Angelico <rosuav@gmail.com> wrote:
Surely "names" would also be eagerly bound, for use in the "for" loop? [...]
Related objection - when used to name subexpressions in a comprehension (one of the original motivating use cases for this proposal), this introduces an asymmetry which actually makes the comprehension harder to read. As a result, it's quite possible that people won't want to use assignment expressions in this case, and the use case of precalculating expensive but multiply used results in comprehensions will remain unanswered. I think the response here is basically the same as the above - if you don't like them, don't use them. But I do think the additional nuance of "we might not have solved the original motivating use case" is worth a specific response. Overall, I like this much better than the previous proposal. I'm now +1 on the semantic changes to comprehensions, and barely +0 on the assignment expression itself (I still don't think assignment expressions are worth it, and I worry about the confusion they may cause for beginners in particular). Paul

On Wed, Apr 11, 2018 at 6:55 PM, Paul Moore <p.f.moore@gmail.com> wrote:
Yep, exactly. Have corrected the example, thanks.
The PEP has kinda pivoted a bit since its inception, so I'm honestly not sure what "original motivating use case" matters. :D I'm just lumping all the use-cases together at the same priority now.
Now that they have the same semantics as any other form of assignment, they're a bit less useful in some cases, a bit more useful in others, and a lot easier to explain. The most confusing part, honestly, is "why do we have two ways to do assignment", which is why that is specifically answered in the PEP. ChrisA

Overall, I'm slightly negative on this. I think named expressions will be a good thing to have, but not in this form. I'll say up front that, being fully aware of the issues surrounding the introduction of a new keyword, something like a let expression in Haskell would be more readable than embedded assignments in most cases. In the end, I suspect my `let` proposal is a nonstarter and just useful to list with the rest of the rejected alternatives, but I wanted.
Abstract ========
[...]
Rationale =========
[...]
# Even complex expressions can be built up piece by piece y = ((eggs := spam()), (cheese := eggs.method()), cheese[eggs])
I find the assignments make it difficult to pick out what the final expression looks like. The first isn't too bad, but it took me a moment to figure out what y was. Quick: is it * (a, b, c) * (a, (b, c)) * ((a, b), c) * something else First I though it was (a, b, c), then I thought it was actually ((a, b), c), before carefully counting the parentheses showed that I was right the first time. These would be clearer if you could remove the assignment from the expression itself. Assuming "let" were available as a keyword, x = (let eggs = spam().ham in "default" if eggs is None else eggs) y = (let eggs = spam(), cheese = eggs.method() in (eggs, cheese, cheese[eggs])) Allowing for differences in how best to format such an expression, the final expression is clearly separate from its component assignment. (More on this in the Alternative Spellings section below.)
There's no rationale given for why this must be parenthesized. If := were right-associative, assert 0 == (x := y := z := 0) would work fine. (With high enough precedence, the remaining parentheses could be dropped, but one would probably keep them for clarity.) I think you need to spell out its associativity and precedence in more detail, and explain why the rationale for the choice made.
There's no reason give for why this is invalid. I assume it's a combination of 1) Having both += and +:=/:+= would be redundant and 2) not wanting to add 11+ new operators to the language.
Otherwise, the semantics of assignment are unchanged by this proposal.
[List comprehensions deleted]
[existing alternatives redacted]
# Using a temporary name stuff = [[y := f(x), x/y] for x in range(5)]
Again, this would be clearer if the assignment were separated from the expression where it would be used. stuff = [let y = f(x) in [y, x/y] for x in range(5)]
These are the most compelling examples so far, doing the most to push me towards a +1. In particular, my `let` expression is too verbose here: while let data = sock.read() in data: print("Received data:", data) I have an idea in the back of my head about `NAME := FOO` being syntactic sugar for `let NAME = FOO in FOO`, but it's not well thought out.
4. Adding a ``let`` expression to create local bindings value = let x = spam(1, 4, 7, q) in x**2 + 2*x 5. Adding a ``where`` expression to create local bindings: value = x**2 + 2*x where x = spam(1, 4, 7, q) Both have the extra-keyword problem. Multiple bindings are little harder to add than they would be with the ``where:`` modifier, although a few extra parentheses and judicious line breaks make it not so bad to allow a comma-separated list, as shown in my first example at the top of this reply.
4. `` let NAME = EXPR1 in EXPR2``:: stuff = [let y = f(x) in (y, x/y) for x in range(5)] I don't have anything new to say about this. It has the same keyword objections as similar proposals, and I think I've addressed the use case elsewhere.
I don't find this convincing. I don't really see chained assignments often enough to worry about how they are written, plus note my earlier question about the precedence and associativity of :=. The fact is, `x := 5` as an expression statement appears equivalent to the assignment statement `x = 5`, so I suspect people will start using it as such no matter how strongly you suggest they shouldn't. -- Clint

On 11 April 2018 at 13:23, Clint Hepner <clint.hepner@gmail.com> wrote:
# Even complex expressions can be built up piece by piece y = ((eggs := spam()), (cheese := eggs.method()), cheese[eggs])
This is a reasonable concern, IMO. But it comes solidly under the frequently raised objection "This could be used to create ugly code!". Writing it as y = ( (eggs := spam()), (cheese := eggs.method()), cheese[eggs] ) makes it obvious what the structure is. Paul

On Wed, Apr 11, 2018 at 10:23 PM, Clint Hepner <clint.hepner@gmail.com> wrote:
I have no idea what the "in" keyword is doing here, but somehow it isn't being used for the meaning it currently has in Python. Does your alternative require not one but *two* new keywords?
It's partly because of other confusing possibilities, such as its use inside, or capturing, a lambda function. I'm okay with certain forms requiring parens.
And 3) there's no point. Can you give an example of where you would want an expression form of augmented assignment?
Both also have the problem of "exactly how local ARE these bindings?", and the 'let' example either requires two new keywords, or requires repurposing 'in' to mean something completely different from its usual 'item in collection' boolean check. The 'where' example is broadly similar to rejected alternative 3, except that you're removing the colon and the suite, which means you can't create more than one variable without figuring some way to parenthesize. Let's suppose this were defined as: EXPR where NAME = EXPR as a five-component sequence. If you were to write this twice EXPR where NAME = EXPR where OTHERNAME = EXPR then it could just as logically be defined as "EXPR where NAME = (EXPR where OTHERNAME = EXPR)" as the other way. And even if it were to work as "(EXPR where NAME = EXPR) where OTHERNAME = EXPR", that still has the highly confusing semantics of being evaluated right-to-left. (Before you ask: no, you can't define it as "EXPR where NAME = EXPR , NAME = EXPR", because that would require looking a long way forward.)
This section is specifically about proposals that ONLY solve this problem within list comprehensions. I don't think there's any point mentioning your proposal there, as the "let NAME = EXPR in EXPR" notation has nothing particularly to do with comprehensions.
If you don't use them, why would you care either way? :)
Probably. But when they run into problems, the solution will be "use an assignment statement, don't abuse the assignment expression". If you want to, you can write this: np = __import__("numpy") But it's much better to use the import statement, and people will rightly ask why you're using that expression form. Your "let... in" syntax is kinda interesting, but has a number of problems. Is that the exact syntax used in Haskell, and if so, does Haskell use 'in' to mean anything else in other contexts? ChrisA

On 11 April 2018 at 14:25, Chris Angelico <rosuav@gmail.com> wrote:
The only possible reading of x := y := z := 0 is as x := (y := (z := 0)) because an assignment expression isn't allowed on the LHS of :=. So requiring parentheses is unnecessary. In the case of an assignment statement, "assignment to multiple targets" is a special case, because assignment is a statement not an expression. But with assignment *expressions*, a := b := 0 is simply assigning the result of the expression b := 0 (which is 0) to a. No need for a special case - so enforced parentheses would *be* the special case. And you can't really argue that they are needed "for clarity" at the same time as having your comments about how "being able to write ugly code" isn't a valid objection :-) Paul

On Wed, Apr 11, 2018 at 11:37 PM, Paul Moore <p.f.moore@gmail.com> wrote:
Sure, if you're just assigning zero to everything. But you could do that with a statement. What about this: q = { lambda: x := lambda y: z := a := 0, } Yes, it's an extreme example, but look at all those colons and tell me if you can figure out what each one is doing. ChrisA

On 11 April 2018 at 14:54, Chris Angelico <rosuav@gmail.com> wrote:
lambda: x := (lambda y: (z := (a := 0))) As I say, it's the only *possible* parsing. It's ugly, and it absolutely should be parenthesised, but there's no need to make the parentheses mandatory. (And actually, it didn't take me long to add those parentheses, it's not *hard* to parse correctly - for a human). Paul

On Thu, Apr 12, 2018 at 12:11 AM, Paul Moore <p.f.moore@gmail.com> wrote:
Did you pick up on the fact that this was actually in a set? With very small changes, such as misspelling "lambda" at the beginning, this actually becomes a dict display. How much of the expression do you need to see before you can be 100% sure of the parsing? Could you do this if fed tokens one at a time, with permission to look no more than one token ahead? ChrisA

On 11 April 2018 at 15:28, Chris Angelico <rosuav@gmail.com> wrote:
Yes. It's not relevant to the parsing. It is relevant to the possibility of errors, as you point out. But once again, it's not the role of the PEP to prevent people writing bad code. Anyway, this is mostly nitpicking. I'm not trying to argue that this is good code, or robust code, or even code that I'd permit within a mile of one of my programs. All I'm trying to say is that *if* you want to state that the parentheses are mandatory in chained assignment expressions, then I think you need to justify it (and my suspicion is that you don't have a good justification other than "it prevents bad code" - which is already covered by the part of the PEP that points out that it's not the job of this PEP to prevent people writing bad code ;-)). Personally, I dislike the tendency with recent syntax proposals to mandate parentheses "to remove ambiguity". In my experience, all that happens is that I end up never knowing whether parentheses are required or not - and as a result end up with too many parentheses, making my code look ugly and encouraging cargo cult style "better add parens just in case" behaviour (as opposed to the reasonable rule "add parens for readability"). Paul

On Thu, Apr 12, 2018 at 12:28:23AM +1000, Chris Angelico wrote:
I agree with Paul, except I think he's added too many parens. Chained assignments ought to be obvious enough that we can dispense with the extras: lambda: x := (lambda y: (z := a := 0)) I know that they are legal, but I really dislike *pointless* examples that bind to a name and then never use it. If we're to get a good feel for how complex these expressions are going to be, they ought to be realistic -- even if that makes them more complex. And I'm not terribly disturbed by excessively obfuscated examples. The answer to obfuscated code is, Don't Do That. So we should consider complex examples which are *realistic*, not ones designed intentionally as obfuscated code. So, with that advice, let's take your q example from above, and re-work it into something which is at least potentially realistic, of a sort. We want q to be a set consisting of a factory function which takes a single argument (different from your example, I know), builds an inner function, then returns that function and the result of that function called with the original argument: def factory(arg): def inner(y): a := z := y + 1 # seems kinda pointless to me, but okay... return (a, a+z, a*z) return (inner, inner(arg)) q = {1, 2, factory, 3, 4} Now let's re-write it in using expression assignment: q = {1, 2, (lambda arg: lambda y: (a := (z := y + 1), a+z, z*z) ), 3, 4, } Not too awful, although it is kinda pointless and not really a great justification for the feature. Let's obfuscate it: q = {1, 2, (lambda arg: lambda y: a := z := y + 1, a+z, z*z), 3, 4} I've seen worse :-) -- Steve

Just one; I don't see using ``in`` here to be any more or a problem than was reusing ``if``, ``then``, and ``else`` for conditional expressions. Its purpose is to separate the bindings from the expression they are used in; eggs and cheese are only valid in the expression that follows "in". Syntactically, this is pretty much identical to Haskell's "let" expression, but the idea of an expression with local bindings is much older. (https://en.wikipedia.org/wiki/Let_expression)
I wouldn't want one :). I'm just suggesting that the PEP include something to the effect of "We're not adding augmented assignment expressions because...".
``in`` already has two different uses: as a Boolean operator (two, actually, with ``not in``) and as part of the various ``for`` constructs. IMO, I don't see adding this third meaning to be a problem. With ``let``, the scope extends as far right of ``in` as possible: let NAME = EXPR in let OTHERNAME = EXPR in EXPR is equivalent to let NAME = EXPR in (let OTHERNAME = EXPR in EXPR)
I agree that `where` *should* be rejected; I don't really like them in Haskell, either. I only listed ``let`` here because I assume it *will* be rejected due to its requiring a new keyword, no matter how much I think adding a new keyword is warranted.
Fair enough, although that suggests a proper let expression precludes the need for any special handling.
I just mean this seems like a weak argument if you are trying to convince someone to use assignment statements. "Assuming I never use chained assignments, why should I use ``=`` instead of ``:=`?"
All the dunder methods exist to support a higher-level syntax, and are not really intended to be used directly, so I think a stronger argument than "Don't use this, even though you could" would be preferable.
I don't believe ``in`` is used elsewhere in Haskell, although Python already has at least two distinct uses as noted earlier. In Haskell, the ``let`` expression (like much of Haskell's syntax) is syntactic sugar for an application of lambda abstraction. The general form let n1 = e1 n2 = e2 in e3 is syntactic sugar for let n1 = e1 in let n2 = e2 in e3 where multiple bindings are expanded to a series of nested expressions. The single expression let n1 = e1 in e2 itself is transformed into (\n1 -> e2) e1 (or translated to Python, (lambda n1: e2)(e1)). -- Clint

On Thu, Apr 12, 2018 at 12:46 AM, Clint Hepner <clint.hepner@gmail.com> wrote:
A 'for' loop has the following structure: for targets in expr: To the left of the 'in', you have a list of assignment targets. You can't assign to anything with an 'in' in it:
(Removing the parentheses makes this "for x in (y in [1])", which is perfectly legal, but has no bearing on this discussion.) In contrast, the way you're using it here, it's simply between two arbitrary expressions. There's nothing to stop you from using the 'in' operator on both sides of it: let x = (a in b) in (b in c) This would make the precedence tables extremely complicated, or else have some messy magic to make this work.
Does the document really need to say that it isn't needed?
In round 3 of this PEP, I was focusing on listing all plausible variants. I'm now focusing more on an actually-viable proposal, so myriad alternatives aren't as important any more.
Fair enough. The most important part is the declaration of intent. By using an assignment *statement*, you're clearly showing that this was definitely intentional.
Makes sense. And if someone actually wants expression-local name bindings, this is the one obvious way to do it (modulo weirdness around class scope). This would not solve the if/while situation, and it wouldn't solve several of the other problems, but it does have the advantage of logically being expression-local. ChrisA

On 2018-04-11 05:23, Clint Hepner wrote:
I find the assignments make it difficult to pick out what the final expression looks like.
I strongly agree with this, and for me I think this is enough to push me to -1 on the whole proposal. For me the classic example case is still the quadratic formula type of thing: x1, x2 = (-b + sqrt(b**2 - 4*a*c))/2, (-b - sqrt(b**2 - 4*a*c))/2 It just doesn't seem worth it to me to create an expression-level assignment unless it can make things like this not just less verbose but at the same time more readable. I don't consider this more readable: x1, x2 = (-b + sqrt(D := b**2 - 4*a*c)))/2, (-b - sqrt(D))/2 . . . because having to put the assignment inline creates a visual asymmetry, when for me the entire goal of an expression-level statement is to make the symmetry between such things MORE obvious. I want to be able to write: x1, x2 = (-b + sqrt(D)))/2, (-b - sqrt(D))/2 ... . . . where "..." stands for "the part of the expression where I define the variables I'm re-using in multiple places in the expression". The new proposal does at least have the advantage that it would help with things like this: while x := some_function_call(): # do stuff So maybe I'm -0.5 rather than -1. But it's not just that this proposal "could be used to create ugly code". It's that using it for expression-internal assignments WILL create ugly code, and there's no way to avoid it. I just don't see how this proposal provides any way to make things like the quadratic formula example above MORE readable. -- Brendan Barnwell "Do not follow where the path may lead. Go, instead, where there is no path, and leave a trail." --author unknown

On 2018-04-11 11:05, David Mertz wrote:
That's clever, but why bother? I can already do this with existing Python: D = b**2 - 4*a*c x1, x2 = (-b + sqrt(D)))/2, (-b - sqrt(D))/2 If the new feature encourages people to do something like your example (or my earlier examples with the D definition inline in the expression for x1), then I'd consider that another mark against it. -- Brendan Barnwell "Do not follow where the path may lead. Go, instead, where there is no path, and leave a trail." --author unknown

Fair enough. I wouldn't actually do what I suggested either. But then, I also wouldn't ever write: x1, x2 = (-b + sqrt(D)))/2, (-b - sqrt(D))/2 [with/where/whatever D=...] If your goal is simply to have symmetry in the plus-or-minus clauses, I was simply pointing out you can have that with the ':=' syntax. Inasmuch as I might like assignment expressions, it would only be in while or if statements, personally. On Wed, Apr 11, 2018, 5:09 PM Brendan Barnwell <brenbarn@brenbarn.net> wrote:

I really like this proposal in the context of `while` loops but I'm lukewarm in other contexts. I specifically like what this would do for repeated calls. Being able to replace all of these md5 = hashlib.md5() with open(filename, 'rb') as file_reader: for chunk in iter(lambda: file_reader.read(1024), b''): md5.update(chunk) md5 = hashlib.md5() with open(filename, 'rb') as file_reader: while True: chunk = file_reader.read(1024) if not chunk: break md5.update(chunk) md5 = hashlib.md5() with open(filename, 'rb') as file_reader: chunk = file_reader.read(1024) while chunk: md5.update(chunk) chunk = file_reader.read(1024) with md5 = hashlib.md5() with open(filename, 'rb') as file_reader: while chunk := file_reader.read(1024): md5.update(chunk) seems really nice. I'm not sure the other complexity is justified by this nicety and I'm really wary of anything that makes comprehensions more complicated; I already see enough comprehension abuse to the point of illegibility. --George On Wed, Apr 11, 2018 at 10:51 AM Brendan Barnwell <brenbarn@brenbarn.net> wrote:

Le 11/04/2018 à 23:34, George Leslie-Waksman a écrit :
I like the new syntax, but you can already do what you want with iter(): md5 = hashlib.md5() with open('/etc/fstab', 'rb') as file_reader: for chunk in iter(lambda: file_reader.read(1024), b''): md5.update(chunk) Anyway, both use case fall short IRL, because you would wrap read in huge try/except to deal with the mess that is letting a user access the filesystem.

On Thu, Apr 12, 2018 at 4:45 PM, Michel Desmoulin <desmoulinmichel@gmail.com> wrote:
That works ONLY if you're trying to check for a sentinel condition via equality. So that'll work for the file read situation, but it won't work if you're watching for any negative number (from APIs that use negative values to signal failure), nor something where you want the condition to be "is not None", etc, etc, etc. Also, it doesn't read nearly as well. ChrisA

On Thu, Apr 12, 2018 at 3:49 AM, Brendan Barnwell <brenbarn@brenbarn.net> wrote:
What if you want to use it THREE times? roots = [((-b + sqrt(D))/2/a, (-b - sqrt(D))/2/a) for a,b,c in triangles if (D := b**2 - 4*a*c) >= 0] Now it's matching again, without any language changes. (I've reinstated the omitted division by 'a', in case anyone's confused by the translation. It has no bearing on the PEP discussion.) Same if you're using an if statement.
I don't think it's as terrible as you're saying. You've picked a specific example that is ugly; okay. This new syntax is not meant to *replace* normal assignment, but to complement it. There are times when it's much better to use the existing syntax. ChrisA

On Wed, Apr 11, 2018 at 1:49 PM, Brendan Barnwell <brenbarn@brenbarn.net> wrote:
I'd probably write this as: x1, x2 = [(-b + s*sqrt(b**2 - 4*a*c))/(2*a) for s in (1,-1)] Agreed that the PEP doesn't really help for this use case, but I don't think it has to. The main use cases in the PEP seem compelling enough to me. Nathan

Great work Chris! Thank you! I do not know whether this is good or bad, but this PEP considers so many different topics, although closely interrelated with each other. 2018-04-11 8:32 GMT+03:00 Chris Angelico <rosuav@gmail.com>:
I think this change is important one no matter what will be the future of the current PEP. And since it breaks backward compatibility it deserves a separate PEP.
Previously, there was an alternative _operator form_ `->` proposed by Steven D'Aprano. This option is no longer considered? I see several advantages with this variant: 1. It does not use `:` symbol which is very visually overloaded in Python. 2. It is clearly distinguishable from the usual assignment statement and it's `+=` friends There are others but they are minor.
But the ugly code matters, especially when it comes to Python. For me, the ideal option would be the combination of two rejected parts: Special-casing conditional statements
(+ in `while`) combined with this part: 3. ``with EXPR as NAME``::
I see no benefit to have the assignment expression in other places. And all your provided examples use `while` or `if` or some form of comprehension. I also see no problem with `if (re.search(pat, text) as match) is not None:..`. What is the point of overloading language with expression that will be used only in `while` and `if` and will be rejected by style checkers in other places? With kind regards, -gdg

On Wed, Apr 11, 2018 at 11:03 PM, Kirill Balunov <kirillbalunov@gmail.com> wrote:
Well, it was Guido himself who started the sub-thread about classes and comprehensions :) To be honest, the changes to comprehensions are mostly going to be under-the-hood tweaks. The only way you'll ever actually witness the changes are if you: 1) Use assignment expressions inside comprehensions (ie using both halves of this PEP); or 2) Put comprehensions at class scope (not inside methods, but actually at class scope), referring to other names from class scope, in places other than in the outermost iterable 3) Use 'yield' expressions in the outermost iterable of a list comprehension inside a generator function 4) Create a generator expression that refers to an external name, then change what that name is bound to before pumping the generator; depending on the one open question, this may occur ONLY if this external name is located at class scope. 5) Use generator expressions without iterating over them, in situations where iterating might fail (again, depends on the one open question). Aside from the first possibility, these are extremely narrow edge and corner cases, and the new behaviour is generally the more intuitive anyway. Class scope stops being so incredibly magical that it's completely ignored, and now becomes mildly magical such that name lookups are resolved eagerly instead of lazily; and the outermost iterable stops being magical in that it defies the weirdness of class scope and the precise definitions of generator functions. Special cases are being removed, not added.
I'm not sure why you posted this in response to the open question, but whatever. The arrow operator is already a token in Python (due to its use in 'def' statements) and should not conflict with anything; however, apart from "it looks different", it doesn't have much to speak for it. The arrow faces the other way in languages like Haskell, but we can't use "<-" in Python due to conflicts with "<" and "-" as independent operators.
Can you give an example of how your syntax is superior to the more general option of simply allowing "as" bindings in any location? ChrisA

2018-04-11 16:50 GMT+03:00 Chris Angelico <rosuav@gmail.com>:
Can you give an example of how your syntax is superior to the more general option of simply allowing "as" bindings in any location?
This is not my syntax :) And not even my idea. I just do not understand, and even a little skeptical about allowing "as" bindings in **any location** with global scoping. All the examples in this thread and the previous ones, as well as almost all PEP's examples show how this feature will be useful in `if`, `while` statements and comprehension/generator expressions. And it excellently solves this problem. This feature increases the capabilities of these statements and also positively affects the readability of the code and it seems to me that everyone understands what this means in this context without ambiguity in their meaning in `while` or `with` statements. The remaining examples (general ones) are far-fetched, and I do not have much desire to discuss them :) These include: lambda: x := lambda y: z := a := 0 y = ((eggs := spam()), (cheese := eggs.method()), cheese[eggs]) and others of these kind... Thus, I do not understand why to solve such a general and complex problem, when this syntax is convenient only in specific cases. In addition, previously the concept of a Statement-Local Name Bindings was discussed, which I basically like (and it fits the above idea). In this version, it was abandoned completely, but it is unclear for what reasons. p.s.: Maybe someone has use-cases outside `if`, `while` and comprehensions, but so far no one has demonstrated them. With kind regards, -gdg

2018-04-11 18:01 GMT+03:00 Kirill Balunov <kirillbalunov@gmail.com>:
I find that I wrote very vague, so I'll try in response to my answer to add some specifics. In general, I find this idea missed in the language and thank you for trying to fix this! In my opinion it has only a meaning in certain constructions such as `while`, `if`, `elif` and maybe comprehensions\generators. As a general form "anywhere" it can be _useful_, but makes the code unreadable and difficult to perceive while giving not so much benefit. What I find nice to have: Extend while statement syntax: while (input("> ") as command) != "quit": print("You entered:", command) Extend if statement syntax: if re.search(pat, text) as match: print("Found:", match.group(0)) if (re.search(pat, text) as match) is not None: print("Found:", match.group(0)) also `elif` clauses should be extended to support. Extend comprehensions syntax: # Since comprehensions have an if clause [y for x in data if (f(x) as y) is not None] # Also this form without `if` clause [(y, x/y) with f(x) as y for x in range(5)] Extend ternary expression syntax: data = y/x if (f(x) as y) > 0 else 0 I think that is all. And it seems to me that it covers 99% of all the use-cases of this feature. In my own world I would like them to make a local _statement_ binding (but this is certainly a very controversial point). I even like that this syntax matches the `with` an `except` statements syntax, although it has a different semantic. But I do not think that anyone will have problems with perception of this. With kind regards, -gdg

On Thu, Apr 12, 2018 at 5:24 AM, Kirill Balunov <kirillbalunov@gmail.com> wrote:
What you're writing there is not truly an extension of the while statement, but a special feature of an expression *within* the while header. Syntactically, this "expr as name" notation must be able to be combined with other operators (as in what you've written here), so it isn't like the way the 'with' or 'import' statement specifically makes a feature available as its own syntax.
Extend ternary expression syntax:
data = y/x if (f(x) as y) > 0 else 0
Again, this is doing further operations after the capturing, so it's not like you can incorporate it in the syntax. You can't write: expr2 if expr1 as NAME else expr3 and explain its semantics that way, because then you can't put the "> 0" part in anywhere. So if, syntactically, this is a modification to expressions in general, why restrict them to certain contexts? Why can't I lift the condition out of the 'while' and give it a name? while (input("> ") as command) != "quit": # becomes # cond = (input("> ") as command) != "quit" print(cond) while cond: But I can't if this is magic in the 'while' statement. What do you gain by forbidding it?
Statement-local names, controversial? You don't say! I actually think the parallel with 'with' and 'except' works *against* that version of the proposal, precisely because of the different semantics (as you mention). The difference between: except Exception as e: except (Exception as e): is significant and fairly easy to spot; as soon as you try to use 'e', you'll figure out that it's the Exception class, not the instance that got thrown. But in a 'with' statement? with open(fn) as f: with (open(fn) as f): These will do the same thing, because Python's file objects return self from __enter__. So do a lot of context managers. You can go a VERY long way down this rabbit-hole, doing things like: with (open(infile) as read and open(outfile, "w") as write): write.write(read.read()) In CPython, you likely won't notice anything wrong here. And hey, it's backslash-free multi-line context management! In fact, you might even be able to use this in *Jython* without noticing a problem. Until you have two output files, and then stuff breaks badly. Thus I sought to outright forbid 'as' in the expressions used in a 'with' or 'except' statement. The problem doesn't exist with ':=', because it's clear that the different semantics go with different syntax: with open(fn) as f: with f := open(fn): And since there's no reason to restrict it, it's open to all contexts where an expression s needed. ChrisA

2018-04-12 1:43 GMT+03:00 Chris Angelico <rosuav@gmail.com>:
All right! You caught me :) For lack of a thoughtful alternative version in my head, let it be an expression but such that it can only be used (evaluated) in boolean context: `while` and `if`s statements (let's skip comprehensions and generators for some time, except their `if` clause). I agree that it contradicts with the way that in 'with' or 'import' statements it is a part of their own syntax. But nonetheless they have the same syntax but, strictly speaking, different semantics! And this is normal, because they are different statements.
Ability to combine `expr as name` with other operators - is actually a matter of operator precedence. In my mind it should have the highest precedence: while input("> ") as command != "quit": is equivalent to while (input("> ") as command) != "quit": Suppose that some function can return `empty tuple` and `None` (which are both False in boolean context), for this case you can write: while func(something) as value is not None: which is equivalent to while (func(something) as value) is not None: and can be also written as: while (func(something) as value) or (value is not None): In the last snippet parenthesis were used only for readability. Another example: while f1(x) + f2(x) as value: will be parsed as: while f1(x) + (f2(x) as value):
Do not quite understand why I can not? :) (the parenthesis was used only for readability)
I gain readability! I don't see any reason to use it in other contexts... Because it makes the code unreadable and difficult to perceive while giving not so much benefit. I may be wrong, but so far I have not seen a single example that at least slightly changed my mind. Concerning your example, I did not understand it..`cond` is evaluated only once...why you need a while loop in this case?
It **will not be allowed** in `except` since there is no boolean context... There is no parallels, only the same syntax, it seems to me that no one will have problems understanding what that means in different contexts. In addition, at the moment all are coping with the differences between `import`, `with` and `except`.
The same goes for `with` statements, I see no reason to use both `:=` and `expr as name` in with statements. How this feature can be used here, especially in the context you mentioned above?
As for me this example just shows that `:=` should not be used in `with` statements at all. I ask you to understand me correctly, but what surprised me most, was that different versions of PEP (3 and 4) speaks about absolutely different things. I think it would be great to have two competing proposals: 1. This PEP in the form that is now (with general assignment expression) 2. Another PEP which discusses only the changes in if and while statements. I understand that it is much easier to advise than to do! I also understand how much time it takes for all this. I myself still can not find the time (English is given to me with great difficulty :)) to write about PEP about partial assignment statement ... But still, as I see these two PEPs will allow to simultaneously look at the two approaches, and also allow to separately work through the problems arising in each of them. I do not want to repeat it again, but I have not yet seen any reasonable example where this current `:=` feature can be useful, except `while` and `if`. So I do not understand why everything needs to be complicated and not concentrate on what can actually be useful. With kind regards, -gdg

Wouldn't these local name bindings make the current "as" clause of "with f(x) as y" completely obsolete ? It's probably good to know my background, and my background is that I know completely nothing of the implementation, im only junior software engineer, and python was my first programming experience, and still the one I have by far the most experience with. To me, the entire proposal sounds mostly like an expansion of the as syntax as we know it from "with". There will be no difference between: with open(filename) as f: // code and with f := open(filename): // code or at least as far as I can see. (that is, if := will be allowed in the with statement, and it sounds like it will ?) from this (outsiders?) point of view, it'd make a lot more sense to keep using "as" as the local binding operator - that's exactly what it already seems to do, even if it looks different under the hood. This would keep it to just one way to do stuff, and that happens to be the way everyone's used to. of course, should still extend. So right now. with open(os.path.join(path, filename) as full_file_path) as f: // Do stuff with both full_file_path and f wont work, but it'd still be awesome/useful if it did. (overall +1 for the idea, for what it's worth) Jacco

2018-04-12 12:48 GMT+03:00 Jacco van Dorp <j.van.dorp@deonet.nl>:
Thank you Jacob! I do not know if I understood correctly how you understand what is happening here. But you are just demonstrating my fears about this proposal... with f := open(filename): This will be only valid if the returned object of (f := open(filename)) defines __enter__ and __exit__ methods ( Nevertheless, in this situation it is so). But in other cases it will raise an error. Generally `with name := expr` is not equivalent to `with expr as name:`. In another places, for example, `except` clause `f := something` is valid only if the returned type is an object, which inherit from BaseException. So in this two situations, in my opinion, it will not be used too much. Yours example under current proposal should look like `with open( full_file_path := os.path.join(path, filename) ) as f:`. With kind regards, -gdg

2018-04-12 13:28 GMT+02:00 Kirill Balunov <kirillbalunov@gmail.com>:
No, it will not raise an error to replace all these "as" usages with name binding, if we choose operator priority right. Even if we consider "with (y := f(x))", the f(x) the with gets is the same as the one bound to the name - that's the point of the binding expression. The only difference seems to be whether the name binding is done before or after the __enter__ method - depending on operator priority (see below). I've looked through PEP 343, contextlib docs ( https://docs.python.org/3/library/contextlib.html ), and I couldn't find a single case where "with (y := f(x))" would be invalid. The only difference I can think of is objects where __enter__ doesn't return self. Inexperienced programmers might forget it, so we're giving them the "as y" part working instead of binding y to None. There might be libraries out there that return a non-self value from __enter__, which would alter behaviour. I honestly can't imagine why you might do that tho. And this could be entirely solved by giving "with" a higher priority than "as". Then if "as" was used instead of ":=", you could just drop "as" as part of the with statement, and it'd work the exact same way everyone's used to. And it's basically the same with "except x as y"; If except gets a higher priority than as, it'd do the same thing(i.e., the exception object gets bound to y, not the tuple of types). At least, that's what it'd look like from outside. If people'd complain "but the local binding does different behind except", you explain the same story as when they would ask about the difference between + and * priority in basic math - and they'd be free to use parenthesis as well if they really want to for some reason i'm unable to comprehend. except (errors := (TypeError, ValueError)) as e: # Or of course: except ( (TypeError, ValueError) as errors ) as e: logger.info(f"Error {e} is included in types: {errors}") Where it all comes together is that if as is chosen instead of :=, it might just be far easier to comprehend for people how it works. "same as in with" might be wrong technically, but it's simple and correct conceptually. Note: I'm talking about operator priority here as if with and except return anything - which I know they don't. That probably complicates it a lot more than it sounds like what I've tried to explain my thoughts here, but I hope I made sense. (it's obvious to me, but well, im dutch..(jk)) Jacco

On 12 April 2018 at 22:22, Jacco van Dorp <j.van.dorp@deonet.nl> wrote:
Consider this custom context manager: @contextmanager def simple_cm(): yield 42 Given that example, the following code: with cm := simple_cm() as value: print(cm.func.__name__, value) would print "'simple_cm 42", since the assignment expression would reference the context manager itself, while the with statement binds the yielded value. Another relevant example would be `contextlib.closing`: that returns the passed in argument from __enter__, *not* self. And that's why earlier versions of PEP 572 (which used the "EXPR as NAME" spelling) just flat out prohibited top level name binding expressions in with statements: "with (expr as name):" and "with expr as name:" were far too different semantically for the only syntactic difference to be a surrounding set of parentheses. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

2018-04-12 15:02 GMT+02:00 Nick Coghlan <ncoghlan@gmail.com>:
Makes sense. However, couldn't you prevent that by giving with priority over the binding ? As in "(with simple_cm) as value", where we consider the "as" as binding operator instead of part of the with statement ? Sure, you could commit suicide by parenthesis, but by default it'd do exactly what the "with simple_cm as value" currently does. This does require use of as instead of :=, though. (which was the point I was trying to make, apologies for the confusion)

On Thu, Apr 12, 2018 at 11:31 PM, Jacco van Dorp <j.van.dorp@deonet.nl> wrote:
If you want this to be a generic name-binding operation, then no; most objects cannot be used as context managers. You'll get an exception if you try to use "with 1 as x:", for instance. As Nick mentioned, there are context managers that return something other than 'self', and for those, "with expr as name:" has an important meaning that cannot easily be captured with an assignment operator. ChrisA

On Thu, Apr 12, 2018 at 7:21 PM, Kirill Balunov <kirillbalunov@gmail.com> wrote:
This is, in effect, your entire argument for permitting assignments only in certain contexts. "I can't think of any useful reason for doing this, so we shouldn't do it". But that means making the language grammar more complicated (both in the technical sense of the parser's definitions, and in the colloquial sense of how you'd explain Python to a new programmer), because there are these magic constructs that can be used anywhere in an expression, but ONLY if that expression is inside an if or while statement. You lose the ability to refactor your code simply to satisfy an arbitrary restriction to appease someone's feeling of "it can't be useful anywhere else". There are basically two clean ways to do this: 1) Create actual syntax as part of the while statement, in the same way that the 'with EXPR as NAME:' statement does. This means you cannot put any additional operators after the 'as NAME' part. It's as much a part of the statement's syntax as the word 'in' is in a for loop. 2) Make this a feature of expressions in general. Then they can be used anywhere that an expression can be. I've gone for option 2. If you want to push for option 1, go ahead, but it's a nerfed solution just because you personally cannot think of any good use for this. ChrisA

On Wed, Apr 11, 2018 at 11:50:44PM +1000, Chris Angelico wrote:
On the contrary, it puts the expression first, where it belongs *semi-wink*. The expression is the most important part of the assignment expression, and because we read from left to right, it should come first. Let's take a simple example: pair = (first_value := x + y + z, a + b + first_value ) What's the first item of the pair? If you're like me, and I think most people are similar, when skimming the code, you read only far across each line to get an idea of whether it is relevant or not. In this case, when skimming, you have to read past the name, past the assignment operator, and only then do you see the relevant information. Contrast: pair = (x + y + z -> first_value, a + b + first_value ) Now you need only read *up to* the assignment operator. Now clearly a careful and detailed reading of the code requires just as much work either way, but we often skim code, especially trying to identify the section that needs careful reading. I know that it is a long standing convention in programming and mathematics to write assignments with the variable on the left, but when I'm sketching out code on paper, I often *start* with the expression: x + y + z and only then do I go back and squeeze in a name binding on the left. (Especially since half the time I'm not really sure what the variable should be called. Naming things is hard.) Or I just draw in an arrow pointing to the name on the right: x + y + z ----> name
The arrow faces the other way in languages like Haskell,
Indeed, but in R, it faces to the right. (Actually, R allows both direction.) There's also apparently a language BETA which uses -> for assignment, although I've never used it. HP calculator "RPN" language also includes a -> assignment operator for binding named parameters (taken off the stack) inside functions, except they use a custom encoding with an arrow symbol, not a literal hyphen+greater-than sign. Likewise for TI Nspire calculators, which also use an right-pointing arrow assignment operator. (They also have a Pascal-style := operator, so you're covered both ways.) This comes from various dialects of calculator BASIC. -- Steve

On Fri, Apr 13, 2018 at 10:22 PM, Steven D'Aprano <steve@pearwood.info> wrote:
The 'as' syntax already has that going for it. What's the advantage of the arrow over the two front-runners, ':=' and 'as'?
Yet Python has an if/else operator that, in contrast to C-inspired languages, violates that rule. So it's not a showstopper. :)
I looked up R's Wikipedia page and saw only the left-facing arrow. How common is the right-facing arrow? Will people automatically associate it with name binding?
So we have calculators, and possibly R, and sorta-kinda Haskell, recommending some form of arrow. We have Pascal and its derivatives recommending colon-equals. And we have other usage in Python, with varying semantics, recommending 'as'. I guess that's enough to put the arrow in as another rejected alternate spelling, but not to seriously consider it. ChrisA

On 13 April 2018 at 22:35, Chris Angelico <rosuav@gmail.com> wrote:
I stumbled across https://www.hillelwayne.com/post/equals-as-assignment/ earlier this week, and I think it provides grounds to reconsider the suitability of ":=", as that symbol has historically referred to *re*binding an already declared name. That isn't the way we're proposing to use it here: we're using it to mean both implicit local variable declaration *and* rebinding of an existing name, the same as we do for "=" and "as". I think the "we already use colons in too many unrelated places" argument also has merit, as we already use the colon as: 1. the header terminator when introducing a nested suite 2. the key:value separator in dictionary displays and comprehensions 3. the name:annotation separator in function parameter declarations 4. the name:annotation separator in variable declarations and assignment statements 5. the parameter:result separator in lambda expressions 6. the start:stop:step separator in slice syntax "as" is at least more consistently associated with name binding, and has fewer existing uses in the first place, but has the notable downside of being thoroughly misleading in with statement header lines, as well as being *so* syntactically unobtrusive that it's easy to miss entirely (especially in expressions that use other keywords). The symbolic "right arrow" operator would be a more direct alternative to the "as" variant that was more visually distinct: # Handle a matched regex if (pattern.search(data) -> match) is not None: ... # More flexible alternative to the 2-arg form of iter() invocation while (read_next_item() -> item) is not None: ... # Share a subexpression between a comprehension filter clause and its output filtered_data = [y for x in data if (f(x) -> y) is not None] # Visually and syntactically unambigous in with statement headers with create_cm() -> cm as enter_result: ... (Pronunciation-wise, if we went with that option, I'd probably pronounce "->" as "as" most of the time, but there are some cases like the "while" example above where I'd pronounce it as "into") The connection with function declarations would be a little tenuous, but could be rationalised as: Given the function declation: def f(...) -> Annotation: ... Then in the named subexpression: (f(...) -> name) the inferred type of "name" is "Annotation" Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Fri, Apr 13, 2018 at 7:36 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
I've not done much research about this topic, but I lived through it (my first languages were Algol-60 And Fortran=IV, in 1974, soon followed by Pascal and Algol-68) and I think that blog post is overly biased by one particular thread of C's ancestry. CPL, BCPL and B were bit players in the world of languages (I suspect mostly focused on Bell Labs and/or MIT, at least US east coast). I should probably blog about my own view of this history, but basically I don't believe that the distinction between initialization and re-assignment is the big decider here. Python historically doesn't care, all its assignments work like dict[key] = value (with a slight exception for the analyses related to local scopes and closures).
But := is not a colon -- it's a new symbol spelled as two characters. The lexer returns it as a single symbol, like it does != and ==. And we're lucky in the sense that no expression or statement can start with =, so there is no context where : = and := would both be legal.
Right -- 'as' signals that there's something funky happening to its left argument before the assignment is made.
I am not excited about (expr -> var) at all, because the existing use of -> in annotations is so entirely different. -- --Guido van Rossum (python.org/~guido)

On Sat, Apr 14, 2018 at 12:36 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
I'm not bothered by that. Assignment semantics vary from one language to another; the fact that Python marks as local anything that's assigned to is independent of the way you assign to it. ("print(x)" followed by "for x in ..." is going to bomb with UnboundLocalError, for instance.) If Python had any form of local variable declarations, it wouldn't change the behaviour of the := operator. ChrisA

On Fri, Apr 13, 2018 at 10:35:59PM +1000, Chris Angelico wrote:
Personally, I like "as" better than -> since English-like expressions and syntax is nicer than symbols. (Up to a point of course -- we surely don't want COBOL-like "ADD 1 TO x" syntax.) The arrow also completely bypasses the entire with/except problem. So my position is: - "as" is more Pythonic and looks nicer; - but it requires a compromise to avoid the with/except problem; - I'm okay with that compromise, but if others aren't, my second preference is the arrow binding operator -> - which also has the advantage that it is completely unused apart from function annotations; - and if people don't like that, I'm okay with := as a distant third choice. (But see below.)
A ternary operator can't put *all* three clauses first. Only one can go first. And you know which one we picked? Not the condition, as C went with. Not the alternative "else" expression. But the primary "if" clause, in other words, the expression that we consider to be the usual case. So the ternary if supports my position, it isn't in opposition :-) But of course your general observation is correct. "Expression first" is violated by regular assignment, by long tradition. I believe that was inherited from mathematics, where is may or may not make sense, but either way it is acceptible (if only because of long familiarity!) for assignment statements. But we should reconsider it for expressions. Analogy: we write "with expression as name" for context managers. We could have put the name first and written "using name from expression", but that puts the name and expression in the wrong order. Similarly we don't write "import as np numpy", rather we use "import numpy as np". We put the entity being imported first, the name it is bound to last. But again, I acknowledge that there are exceptions, like for loops, and they've been around a long time. Back to the 1950s and the invention of Fortran. So no, this isn't an absolute showstopper.
I don't interact with the R community enough to know how commonly people use -> versus <- but you can certainly try it for yourself in the R interpreter if you have any doubts that it works. The statistician John Cook says -> is "uncommon": https://www.johndcook.com/blog/r_language_for_programmers/#assignment but in any case, I think that the idea of arrows as pointers, motion, "putting into" and by analogy assignment shouldn't be hard to grasp. The first time I saw pseudo-code using <- for assignment, I was confused by why the arrow was pointing to the left instead of the right but I had no trouble understanding that it implied taking the value at non-pointy end and moving it into the variable at the pointy end.
Well, it's your PEP, and I can't force you to treat my suggestion seriously, but I am serious about it and there have been a few other people agree with me. I grew up with Pascal and I like it, but it's 2018 and a good twenty to thirty years since Pascal was a mainstream language outside of academia. In 1988, even academia was slowly moving away from Pascal. I think the death knell of Pascal as a serious mainstream language was when Apple stopped using Pascal for their OS and swapped to C for System 7 in 1991. The younger generation of programmers today mostly know Pascal only as one of those old-timer languages that is "considered harmful", if even that. And there is an entire generation of kids growing up using CAS calculators for high school maths who will be familiar with -> as assignment. So in my opinion, while := is a fine third-choice, I really think that the arrow operator is better. -- Steve

On 2018-04-13 08:44, Steven D'Aprano wrote:
So my position is:
- "as" is more Pythonic and looks nicer;
Yes, perhaps if typing annotations had not chosen the colon but used a whitespace delimiter instead, adding a few more colons to the source would not be an issue. But in combination, it feels like there will be way too many colons in a potential future Python. One of the things I liked about it historically was its relative lack of punctuation and use of short words like in, and, not, as, etc. As mentioned before, I liked := for assignment in Pascal, but presumably since we are keeping == for testing, there's not as strong of an argument for that spelling. -Mike

On Sat, Apr 14, 2018 at 1:44 AM, Steven D'Aprano <steve@pearwood.info> wrote:
Which is also the most important clause in some situations, since it's the governing clause. In an if *statement*, it's the one in the header, before the indented block. I don't think there's enough logic here to draw a pattern from.
I don't know about professional-level mathematics, but certainly what I learned in grade school was conventionally written with the dependent variable before the equals sign. You'd write "y = x²" to graph a parabola, not "x² = y". So the programming language convention of putting the assignment target first was logical to me.
but in any case, I think that the idea of arrows as pointers, motion, "putting into" and by analogy assignment shouldn't be hard to grasp.
Agreed. Whichever way the arrow points, it's saying "the data flows thattaway". C++ took this to a cute level with "cout << blah", abusing the shift operators to signal data flow, and it's a nuisance because of operator precedence sometimes, but nobody can deny that it makes intuitive sense.
(Now I want to use "pointy end" somewhere in the PEP. Just because.)
If I were to take a poll of Python developers, offering just these three options: 1) TARGET := EXPR 2) EXPR as TARGET 3) EXPR -> TARGET and ask them to rank them in preferential order, I fully expect that all six arrangements would have passionate supporters. So far, I'm still seeing a strong parallel between expression-assignment and statement-assignment, to the point of definitely wanting to preserve that. ChrisA

On 11 April 2018 at 15:32, Chris Angelico <rosuav@gmail.com> wrote:
Thanks for putting this revised version together! You've already incorporated my feedback on semantics, so my comments below are mostly about the framing of the proposal in the context of the PEP itself.
Leading with these kinds of examples really doesn't help to sell the proposal, since they're hard to read, and don't offer much, if any, benefit over the status quo where assignments (and hence the order of operations) need to be spelled out as separate lines. Instead, I'd suggestion going with the kinds of examples that folks tend to bring up when requesting this capability: # Handle a matched regex if (match := pattern.search(data)) is not None: ... # A more explicit alternative to the 2-arg form of iter() invocation while (value := read_next_item()) is not None: ... # Share a subexpression between a comprehension filter clause and its output filtered_data = [y for x in data if (y := f(x)) is not None] All three of those examples share the common characteristic that there's no ambiguity about the order of operations, and the latter two aren't amenable to simply being split out into separate assignment statements due to the fact they're part of a loop. A good proposal should have readers nodding to themselves and thinking "I could see myself using that construct, and being happy about doing so", rather than going "Eugh, my eyes, what did I just read?" :)
"names" would also be eagerly bound here.
The example using the PEP syntax could be listed first in its own section, and then the others given as "These are the less obvious alternatives that this new capability aims to displace". Similar to my suggestion above, you may also want to consider making this example a filtered comprehension in order to show the proposal in its best light: results = [(x, y, x/y) for x in input_data if (y := f(x) )]
Similar to the comprehension section, I think this part could benefit from switching the order of presentation.
Frequently Raised Objections ============================
There needs to be a subsection here regarding the need to call `del` at class and module scope, just as there is for loop iteration variables at those scopes.
This argument will be strengthened by making the examples used in the PEP itself more attractive, as well as proposing suitable additions to PEP 8, such as: 1. If either assignment statements or assignment expressions can be used, prefer statements 2. If using assignment expressions would lead to ambiguity about execution order, restructure to use statements instead Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On 11 April 2018 at 16:22, Nick Coghlan <ncoghlan@gmail.com> wrote:
Agreed, this is a *much* better motivating example.
+1 on explicitly suggesting additions to PEP 8. Bonus points for PEP 8 additions that can be automatically checked by linters/style checkers (For example "avoid chained assignment expressions"). Paul

On Thu, Apr 12, 2018 at 1:22 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
Cool, thanks. I've snagged these (and your other examples) and basically tossed them into the PEP unchanged.
Yep, that was a clerical error on my part, now corrected.
Hmm, I'm not sure I follow. Are you saying that this is an objection to assignment expressions, or an objection to them not being statement-local? If the latter, it's really more about "rejected alternative proposals".
Fair enough. Also adding that chained assignment expressions should generally be avoided. Thanks for the recommendations! ChrisA

On 11 April 2018 at 22:28, Chris Angelico <rosuav@gmail.com> wrote:
Another one I think should be included (I'm a bit sad that it's not so obvious that no-one would ever even think of it, but the current discussion pretty much killed that hope for me). * Assignment expressions should never be used standalone - assignment statements should *always* be used in that case. That's also one that I'd like to see implemented as a warning in the common linters and style checkers. I'm still not convinced that this whole proposal is a good thing (the PEP 8 suggestions feel like fighting a rearguard action against something that's inevitable but ill-advised), but if it does get accepted it's in a lot better place now than it was when the discussions started - so thanks for all the work you've done on incorporating feedback. Paul

On 12 April 2018 at 07:28, Chris Angelico <rosuav@gmail.com> wrote:
It's both - accidentally polluting class and module namespaces is an argument against expression level assignments in general, and sublocal namespaces aimed to eliminate that downside. Since feedback on the earlier versions of the PEP has moved sublocal namespaces into the "rejected due to excessive conceptual complexity" box, that means accidental namespace pollution comes back as a downside that the PEP should mention. I don't think it needs to say much, just point out that they share the downside of regular for loops: if you use one at class or module scope, and don't want to export the name, you need to delete it explicitly. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Thu, Apr 12, 2018 at 07:28:28AM +1000, Chris Angelico wrote:
Fair enough. Also adding that chained assignment expressions should generally be avoided.
So long as its not a blanket prohibition, I'm good with that. Also, something like this: spam := 2*(eggs := expression) + 1 should not be considered a chained assignment. -- Steve

On Fri, Apr 13, 2018 at 9:17 PM, Steven D'Aprano <steve@pearwood.info> wrote:
Agreed. Chained assignment is setting the same value to two or more targets, without any further changes or anything: a = b = c = 0 Extremely handy as a statement (quickly setting a bunch of things to zero or None or something), but less useful with expression assignment. Incidentally, I've only just made "x := y := 1" legal tonight (literally during the writing of this post). Previously it needed parentheses, and I just didn't see much priority on fixing something that wasn't all that crucial :) ChrisA

Nick Coghlan writes:
My immediate take was "this syntax is too ugly to live", but so are Gila monsters, and I don't understand the virtues that lead Nick and Guido to take this thread seriously. So I will just leave that statement here. (no vote yet) More constructively, I found it amusing that the results were stuffed into generic one-character variables, while the temporaries got actual words, presumably standing in for mnemonic identifiers. Besides moving the examples, that should be fixed if these examples are to be used at all. I'm also with Paul (IIRC) who suggested formatting the second example on multiple lines. I suggest s/x/filling/ (sandwich) and s/y/omelet/. Steve

On 04/10/2018 10:32 PM, Chris Angelico wrote:
Title: Assignment Expressions
Thank you, Chris, for doing all this! --- Personally, I'm likely to only use this feature in `if` and `while` statements; if the syntax was easier to read inside longer expressions then I might use this elsewhere -- but as has been noted by others, the on-the-spot assignment creates asymmetries that further clutter the overall expression. As Paul noted, I don't think parenthesis should be mandatory if the parser itself does not require them. For myself, I prefer the EXPR as NAME variant for two reasons: - puts the calculation first, which is what we are used to seeing in if/while statements; and - matches already existing expression-level assignments (context managers, try/except blocks) +0.5 from me. -- ~Ethan~

On Thu, Apr 12, 2018 at 4:38 AM, Ethan Furman <ethan@stoneleaf.us> wrote:
Context managers and except blocks don't do the same thing though, so it's a false parallel. The 'as' keyword occurs in Python's grammar thus: 1) "with EXPR as target:" captures the return value from __enter__ 2) "except EXPR as NAME:" captures the exception value, not the type(s) 3) "import NAME as NAME" loads a module object given by a token (name) and captures that 4) "from NAME import NAME as NAME" captures an attribute of a module Three of them use a name, one allows arbitrary assignment targets. (You can "with spam as ham[0]:" but you can't "except Exception as ham[0]:".) Two of them have no expressions at all, so assignment expressions wouldn't logically be usable. The other two are *dangerous* false parallels, because "with foo as bar:" is semantically different from "with (foo as bar):"; it was so awkward to try to explain that away that I actually forbade any use of "(expr as name)" in the header of a with/except block. The parallel is not nearly as useful as it appears to be on first blush. The parallel with assignment statements is far closer; in fact, many situations will behave identically whether you use the colon or not.
- puts the calculation first, which is what we are used to seeing in if/while statements; and
Not sure what you mean here; if and while statements don't have anything BUT the calculation. A 'for' loop puts the targets before the evaluated expression.
- matches already existing expression-level assignments (context managers, try/except blocks)
They're not expression-level assignments though, or else I'm misunderstanding something here? ChrisA

On 04/11/2018 02:55 PM, Chris Angelico wrote:
On Thu, Apr 12, 2018 at 4:38 AM, Ethan Furman wrote:
Context managers and except blocks don't do the same thing though, so it's a false parallel.
They assign things to names using `as`. Close enough. ;)
If `with` is the first token on the line, then the context manager syntax should be enforced -- you get one or the other, not both.
Which is why we're using to seeing it first. ;)
A 'for' loop puts the targets before the evaluated expression.
Hmm, good point -- but it still uses a key word, `in`, to delineate between the two.
No, just me being sloppy with vocabulary. -- ~Ethan~

On Thu, Apr 12, 2018 at 9:03 AM, Ethan Furman <ethan@stoneleaf.us> wrote:
Okay, but because they don't assign the thing just before the word 'as', it's a dangerous parallel. Worse, a 'with' statement OFTEN assigns the thing just before the word 'as', making the parenthesized form frequently, but not always, the same as the unparenthesized.
That's what I had in the previous version: that 'as' bindings are straight-up not permitted in 'with' and 'except' headers. I wasn't able to implement that in the grammar, so I can't demo it for you, but the fact that this was a special case was *itself* a turn-off to some. Are special cases special enough to break the rules? Do we want something that is a subtle bug magnet?
Heh. You could just as easily say you're used to seeing it next to the colon :)
It does, yes. Fortunately we can't have a competing proposal for name bindings to be spelled "NAME in EXPR", because that's already a thing :D
Gotcha, no problem. For myself, I've been back and forth a bit about whether "as" or ":=" is the better option. Both of them have problems. Both of them create edge cases that could cause problems. Since the problems caused by ":=" are well known from other languages (and are less serious than they would be if "=" were the operator), I'm pushing that form. However, the 'as' syntax is a close contender (unlike most of the other contenders), so if someone comes up with a strong argument in its favour, I could switch. ChrisA

On Thu, Apr 12, 2018 at 10:44 AM, Ethan Furman <ethan@stoneleaf.us> wrote:
It can; and in fact, I have a branch where I had exactly that (with the SLNB functionality as well): https://github.com/Rosuav/cpython/tree/statement-local-variables But it creates enough edge cases that I was swayed by the pro-:= lobby. ChrisA

On Wed, Apr 11, 2018 at 03:32:04PM +1000, Chris Angelico wrote:
Have we really decided on spelling this as `target := expression`? You list this as a rejected spelling:
1. ``EXPR as NAME``, with or without parentheses::
stuff = [[f(x) as y, x/y] for x in range(5)]
but I don't think the objections given should be fatal:
The special casing you refer to would be to prohibit name binding expressions in "except" and "with" statements. You should explicitly say so in the PEP. I don't think that prohibiting those two forms is a big loss. I think any form of except (name := expression) as err: do_something(name) is going to be contrived. Likewise for `with` statements. I don't especially dislike := but I really think that putting the expression first is a BIG win for readability. If that requires parens to disambiguate it, so be it. You also missed the "arrow assignment operator" from various languages, including R: expression -> name (In an earlier post, I suggested R's other arrow operator, name <- expr, but of course that already evaluates as unary minus expr.) I think that there should be more attention paid to the idea of putting the expression first, rather than the name. -- Steve

On Fri, Apr 13, 2018 at 9:04 PM, Steven D'Aprano <steve@pearwood.info> wrote:
Parenthesis added to the rejection paragraph.
I agree as regards except statements. Not so much the with statements, though. How many times have people asked for "with (expr as name):" to be supported, allowing the statement to spread over multiple lines? With this syntax, it would suddenly be permitted - with dangerously similar semantics. For many MANY context managers, "with (expr as name):" would do the exact same thing as "with expr as name:". There is a general expectation that adding parentheses to an expression usually doesn't change the behaviour, and if it's legal, people will assume that the behaviour is the same. It isn't, and it's such a sneaky difference that I would call it a bug magnet. So if it's a bug magnet, what do we do? 1) Permit the subtly different semantics, and tell people to be careful 2) Forbid any use of "(expr as name)" in the header of a 'with' statement 3) Forbid it at top level, but permit it deeper down 4) Something else??
There's a mild parallel between "(expr as name)" and other uses of 'as', which bind to that name. Every other use of 'as' is part of a special syntactic form ('import', 'with', and 'except'), but they do all bind to that name. (Point of interest: You can "with expr as x[0]:" but none of the other forms allow anything other than a simple name.) There's a strong parallel between "target := value" and "target = value"; in fact, the section on the differences is incredibly short and could become shorter. The only really important difference is that augmented assignment is not supported (you can't say "x +:= 5"), partly because it'd mean creating a boatload of three-character operators for very little value, and partly because augmented-assignment-as-expression is hard to explain. Which is better? A weak parallel or a strong one? How important is putting the expression first? On balance, I'm currently in favour of the := syntax, but it's only a small difference.
I actually can't find anything about the -> operator, only the <- one. (Not that I looked very hard.) Is it a truly viable competitor, or just one that you'd like to see mentioned for completeness?
I think that there should be more attention paid to the idea of putting the expression first, rather than the name.
How many ways are there to bind a value to a name? Name last: * import x as y * from x import y as z * except x as y * with x as y Name first: * x = y * x += y # etc * for x in y * def x(.....) .... * def f(x=1) - arg defaults * class X: ... I'm seeing consistency here in that *EVERY* name binding where the name is at the end uses "as target" as its syntax. Everything else starts with the target, then defines what's being assigned to it. So I don't see much value in a "->" operator, except for the mere fact that it's different (and thus won't conflict in except/with); and the bulk of name bindings in Python put the name first. I don't have any really strong arguments in favour of :=, but I have a few weak ones and a few not quite so weak. So far, I have only weak arguments in favour of 'as'. ChrisA

On Fri, Apr 13, 2018 at 09:56:35PM +1000, Chris Angelico wrote:
I see your point, but why don't we support "with (expr as name):" to allow multiple lines? No, don't answer that... its off-topic. Forget I asked. In any case, we already allow similar syntax with different meaning in different places, for example, something that looks just like assignment inside expressions: functions = [len, ord, map, lambda x, y=1: x+y] But its not really an assignment as such, its a parameter declaration. If we agree that the benefit of putting the expression first is sufficiently large, or that the general Pythonic look of "expr as name" is sufficiently desirable (it just looks and reads nicely), then we can afford certain compromises. Namely, we can rule that: except expr as name: with expr as name: continue to have the same meaning that they have now and never mean assignment expressions. Adding parens should not change that. If you try to write something like: except (spam or eggs as cheese) or function(cheese) as name: with (spam or eggs as cheese) or function(cheese) as name: etc (or any other assignment expression, effectively anything which isn't currently allowed) then you get a syntax error. So this: with expr as name: process(name) will only work if expr returns an object with a context manager. But that's they way it works now, so nothing really changes. In other words, the rule is that "expr as name" keeps its current, older semantics in with and except statements, and NEVER means the new, PEP 572 assignment expression. Yes, that's a special case that breaks the rules, and I accept that it is a point against "as". But the Zen is a guideline, not a law of physics, and I think the benefits of "as" are sufficient that even losing a point it still wins.
Indeed. I wouldn't allow such a subtle difference in behaviour due to parens. That reminds me of the Python 1 and early 2.x except clauses, where except ValueError, TypeError: except (ValueError, TypeError): meant different things. I still shudder at that one.
So if it's a bug magnet, what do we do?
1) Permit the subtly different semantics, and tell people to be careful
No.
2) Forbid any use of "(expr as name)" in the header of a 'with' statement
You can't forbid it, because it is currently allowed syntax (albeit currently without the parens). So the rule is, it is allowed, but it means what it meant pre-PEP 572.
3) Forbid it at top level, but permit it deeper down
I don't know what that means. But whatever it means, probably no :-)
4) Something else??
Well, there's always the hypothetical -> arrow binding operator, or the Pascal := assignment operator (the current preference). I don't hate the := choice, I just think it is more Pascal-esque that Pythonic :-)
I disagree: I think it is a strong parallel. They're both name bindings. How much stronger do you want? True, we don't currently allow such things as import math as maths, mathematics, spam.modules[0] but we could if we wanted to and there was a sensible use-case for it.
There's a strong parallel between "target := value" and "target = value";
Sure. And for a statement, either form would be fine. I just think that in an expression, it is important enough to bring the expression to the front, even if it requires compromise elsewhere. [...]
Yes, as I mentioned in another post, R allows both -> and <-, some language called BETA uses ->, various calculator BASICs use -> (albeit with a special single character, not a digraph) as does HP RPN. Here's an example from R:
But whether it is viable or not depends on *us*, not what other languages do. No other language choose the syntax of ternary if expression before Python used it. We aren't limited to only using syntax some other language used first.
We shouldn't be choosing syntax because other syntax does the same. We should pick the syntax which is most readable and avoids the most problems. That's why Guido bucked the trends of half a century of programming languages, dozens of modern languages, and everything else in Python, to put the conditional in the middle of ternary if instead of the beginning or end. (And he was right to do so -- I like Python's ternary operator, even if other people think it is weird.) If people agree with me that it is important to put the expression first rather than the target name, then the fact that statements and for loops put the name first shouldn't matter. And if they don't, then I'm outvoted :-) -- Steve

Well this may be crazy sounding, but we could allow left or right assignment with name := expr expr =: name Although it would seem to violate the "only one obvious way" maxim, at least it avoids this overloaded meaning with the "as" of "except" and "with" On Fri, Apr 13, 2018 at 9:29 AM, Ethan Furman <ethan@stoneleaf.us> wrote:

On Fri, Apr 13, 2018 at 11:30 PM, Peter O'Connor <peter.ed.oconnor@gmail.com> wrote:
Hah. It took me multiple readings to even notice the change in the operator there, so I think this would cause a lot of confusion. (Don't forget, by the way, that an expression can be a simple name, and an assignment target can be more complicated than a single name, so it won't always be obvious on that basis.) It probably wouldn't *technically* conflict with anything, but it would get extremely confusing! ChrisA

On Apr 14 2018, Chris Angelico <rosuav-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org> wrote:
Well, if putting the expression first is generally considered better, the reasonable thing to do would be to allow *only* =:. -Best Nikolaus -- GPG Fingerprint: ED31 791B 2C5C 1613 AF38 8B8A D113 FCAC 3C4E 599F »Time flies like an arrow, fruit flies like a Banana.«

On 13 April 2018 at 14:18, Steven D'Aprano <steve@pearwood.info> wrote:
On Fri, Apr 13, 2018 at 09:56:35PM +1000, Chris Angelico wrote:
2) Forbid any use of "(expr as name)" in the header of a 'with' statement
You can't forbid it, because it is currently allowed syntax
It's not currently allowed:
(albeit currently without the parens).
Well, yes, but we're talking about *with* the parens.
So the rule is, it is allowed, but it means what it meant pre-PEP 572.
So it's a syntax error, because that's what it is pre-PEP 572. So it's allowed, but as a syntax error (which is what "not allowed" means". Huh? In any event it's a special case, because "with EXPR:" is valid, and "(12 as x)" is an example of an EXPR, but put these two together and you're saying you should get a syntax error. (Or if you're not, you haven't stated what you *are* proposing...) And there's no good justification for making this a special case (unless you argue in circles: it's worth being a special case because "as" is a good syntax for assignment expressions, and "as" is a good syntax because it's unambiguous...)
I agree that having the expression first is better. And I think that -> deserves consideration on that basis. I think "as" is *not* a good option for a "puts the expression first" option, because of the ambiguities that Chris has explained. But I also think that it's a relatively minor point, and I have bigger reservations than this about the PEP, so arguing over this level of detail isn't crucial to me. Paul

On Fri, Apr 13, 2018 at 02:30:24PM +0100, Paul Moore wrote:
It is without the parens, as I said: [...]
(albeit currently without the parens).
Well, yes, but we're talking about *with* the parens.
You might be, but I'm not. I'm talking about the conflict between: # pre-PEP 572 context manager with expr as name: # post-PEP 572, is it a context manager or an assignment expression? with expr as name: and I'm saying, don't worry about the syntactical ambiguity, just make a ruling that it is a context manager, never an assignment expression. 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.) [...]
I never said that "as" was unambiguous. I just spent a rather long post attempting to explain how to deal with that ambiguity. Sorry if I was not clear enough. I'm saying we can deal with it by simply defining what we want it to mean, and using the surrounding context to distinguish the cases. If it is in a with or except clause (just the header clause, not the following block) , then it means the same thing it means now. Everywhere else, it means assignment expression. Just like "x = 1" can mean assignment, or it can mean a parameter declaration. We don't have different syntax for those two distinct actions, we simply state that if "x = 1" is inside a lambda or function def, then it always means a parameter declaration and never an illegal assignment.
Indeed.
I think "as" is *not* a good option for a "puts the expression first" option, because of the ambiguities that Chris has explained.
I can see why people may dislike the "as" option. I think it comes down to personal taste. My aim is to set out what I see as a way around the with/except problem with a compromise: - allow "expr as name" anywhere; - require parentheses only to avoid syntactic ambiguity; - and simply ban assignment expressions in with/except clauses and keep the existing with/except semantics. I acknowledge that it isn't ideal, but compromises between conflicting requirements rarely are. If people don't agree with me, you're all wrong, er, that is to say, I understand your position even if I disagree *wink* -- Steve

So if I read this correctly, you're making an argument to ignore parens ? 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. Without parens -> old syntax + meaning with parens -> bind expr to name, because that's what the parens say. Ignoring parens, in any way, just seems like a bad idea. If you want to avoid with(expr as name):, shouldn't you make parens illegal ?

On Fri, Apr 13, 2018 at 05:04:00PM +0200, Jacco van Dorp wrote:
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.
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

On Sat, Apr 14, 2018 at 2:43 AM, Steven D'Aprano <steve@pearwood.info> wrote:
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").
The RHS of an assignment is an expression. But you can't move that first '(' any further to the left.
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).
Right.
Sure. And that's a good reason to straight-up *forbid* expression-as-name in a 'with' statement.
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.
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

2018-04-13 23:31 GMT+03:00 Chris Angelico <rosuav@gmail.com>:
Maybe it does not suit you, but what do you think about `SyntaxWarning` instead of `SyntaxError` for both `with` and `except`. By analogy how it was done for `global name` into function body prior to Python 3.6? With kind regards, -gdg

On Sat, Apr 14, 2018 at 7:33 AM, Kirill Balunov <kirillbalunov@gmail.com> wrote:
Warnings are often not seen. For an error this subtle, a warning wouldn't be enough. Good call though; that was one of the considerations, and if we knew for sure that warnings could be seen by the right people, they could be more useful for these cases. ChrisA

On Sat, Apr 14, 2018 at 12:50 AM, Steven D'Aprano <steve@pearwood.info> wrote:
Then we're talking about unrelated points, and "currently-allowed" is immaterial.
That part isn't at all in question. The meaning of "with expr as name:" MUST NOT change, because that would massively break backward compatibility.
For that to work, the assignment expression MUST be disallowed in the header of a 'with' statement. Which is one of the suggestions I made. In fact, it was the PEP's recommendation as of the last point when 'as' was the preferred syntax. I never got as far as implementing that, but it'd be the safest solution. And it's still a special case.
Definitely not; again, that would break backward compatibility. We are in agreement on that.
Then it HAS to be a SyntaxError. Because that's what it means now. Or are you saying "it means the same thing it would mean now if the parentheses were omitted"? Because that is definitely not an easy thing to explain.
If people don't agree with me, you're all wrong, er, that is to say, I understand your position even if I disagree *wink*
Funnily enough, that's my stance too. I guess we're all just wrong, wronger, and wrongest today :) ChrisA

On Fri, Apr 13, 2018 at 11:18 PM, Steven D'Aprano <steve@pearwood.info> wrote:
The answer is simple: for the same reason that you can't parenthesize *most* statements.
It's not an expression, so it doesn't follow the rules of expressions. There is special grammar around the import statement to permit this, and anywhere else, if it's not an expression, parens don't work. (Backslashes do, but that's because they function at a different level.)
That's a different sort of special case from what I had in mind, which is that illegal parenthesization should raise SyntaxError. Either way, though...
... yes, the false parallel is a strike against the "as" syntax.
Agreed.
It isn't currently-allowed syntax precisely because of those parens. So "what it meant pre-PEP 572" is "raise SyntaxError".
3) Forbid it at top level, but permit it deeper down
I don't know what that means. But whatever it means, probably no :-)
That would mean that this is a SyntaxError: with (get_file() as f): But this is allowed: with open(get_filename() as name): It's a more complicated rule, but a more narrow special case (the ONLY thing disallowed is the one thing that would be interpreted differently with and without parens), and there's no ambiguity here.
That's fair. And since Python has the "==" operator for comparison, using a Pascal-style ":=" for assignment isn't really paralleling properly. But purely within Python, any one of the three ('as', ':=', '->') is going to be at least somewhat viable, and it comes down to what they parallel *in Python*: 1) 'as' parallels 'import', 'with', and 'except', which perform name bindings based on some language magic using what's to the left of the 'as' 2) ':=' parallels '=', which takes what's on its right and assigns it to the target on the left 3) '->' parallels function return value annotations, but looks like data's moving from the left to the right.
True, they're name bindings. So are non-renaming import statements ("import os", "from pprint import pprint"), function and class definitions, and for loops. None of those use 'as'. So it's a weak parallel.
I'm not entirely sure what this would do, partly because I'm unsure whether it means to import "math" under the name "maths", and the other two separately, or to import "math" with three targets.
Why is it okay for a statement but not an expression? Genuine question, not scorning it. Particularly since expressions can be used as statements, so "value -> target" could be used as a statement.
Indeed, but for the sake of the PEP, it's useful to cite prior art. Just wanted to clarify.
Fair enough. That said, though, consistency DOES have value. The language fits into people's heads far better if parts of it behave the same way as other parts. The only question is, which consistencies matter the most? ChrisA

On 13 April 2018 at 23:18, Steven D'Aprano <steve@pearwood.info> wrote:
It's not completely off topic. as it's due to the fact we use "," to separate both context managers and items in a tuple, so "with (cm1, cm2, cm3):" is currently legal syntax that means something quite different from "with cm1, cm2, cm3:". While using the parenthesised form is *pointless* (since it will blow up at runtime due to tuples not being context managers), the fact it's syntactically valid makes us more hesitant to add the special case around parentheses handling than we were for import statements. The relevance to PEP 572 is as a reminder that since we *really* don't like to add yet more different cases to "What do parentheses indicate in Python?", we should probably show similar hesitation when it comes to giving ":" yet another meaning. This morning, I remembered a syntax idea I had a while back in relation to lambda expressions that could perhaps be better applied to assignment expressions, so I'll quickly recap the current options, and then go on to discussing that. Since the threads are pretty sprawling, I've also included a postscript going into more detail on my current view of the pros and cons of the various syntax proposals presented so far. Expression first proposals: while (read_next_item() as value) is not None: ... while (read_next_item() -> value) is not None: ... Target first proposal (current PEP): while (value := read_next_item()) is not None: ... New keyword based target first proposal: while (value from read_next_item()) is not None: ... The new one is a fairly arbitrary repurposing of the import system's "from" keyword, but it avoids all the ambiguities of "as", is easier to visually distinguish from other existing expression level keywords than "as", avoids giving ":" yet another meaning, still lets us use a keyword instead of a symbol, and gives the new expression type a more clearly self-evident name ("from expressions", as opposed to the "from statements" used for imports). It also more easily lends itself to skipping over the details of the defining expression when reading code aloud or in your head (e.g. "while value is not None, where value comes from read_next_item()" would be a legitimate way of reading the above for loop header, and you could drop the trailing clause completely when the details aren't particularly relevant). Avoiding the use of a colon as part of the syntax also means that if we wanted to, we could potentially allow optional type annotations in from-expressions ("target: annotation from expression"), and even adopt them as a shorthand for the sentinel pattern in function declarations (more on that below). As far as the connection with "from module import name" goes, given the proposed PEP 572 semantics, these three statements would all be equivalent: from dotted.module import name name = __import__("dotted_module", fromlist=["name"]).name name from __import__("dotted_module", fromlist=["name"]).name Other examples from the PEP: # Handle a matched regex if (match from pattern.search(data)) is not None: ... # Share a subexpression between a comprehension filter clause and its output filtered_data = [y for x in data if (y from f(x)) is not None] # Nested assignments assert 0 == (x from (y from (z from 0))) # Re-using fields in a container display stuff = [[y from f(x), x/y] for x in range(5)] And the set/dict examples display where ":=" could be visually confusing: # Set display with local name bindings data = { value_a from 1, value_b from 2, value_c from 3, } # Dict display with local key & value name bindings data = { key_a from 'a': value_a from 1, key_b from 'b': value_b from 2, key_c from 'c': value_c from 3, } Potential extension to simplifying the optional non-shared-mutable-default-argument pattern: # Shared mutable default (stored directly in f.__defaults__) def f(shared = []): .... # Unshared mutable default (implementation details TBD) def f(unshared from []): .... That last part would only be a potential extension beyond the scope of PEP 572 (since it would go against the grain of "name = expression" and "name from expression" otherwise being functionally equivalent in their behaviour), but it's an opportunity that wouldn't arise if a colon is part of the expression level name binding syntax. Cheers, Nick. P.S. The pros and cons of the current syntax proposals, as I see them: === Expression first, 'as' keyword === while (read_next_item() as value) is not None: ... Pros: * typically reads nicely as pseudocode * "as" is already associated with namebinding operations Cons: * syntactic ambiguity in with statement headers (major concern) * encourages a common misunderstanding of how with statements work (major concern) * visual similarity between "as" and "and" makes name bindings easy to miss * syntactic ambiguity in except clause headers theoretically exists, but is less of a concern due to the consistent type difference that makes the parenthesised form pointless === Expression first, '->' symbol === while (read_next_item() -> value) is not None: ... Pros: * avoids the syntactic ambiguity of "as" * "->" is used for name bindings in at least some other languages (but this is irrelevant to users for whom Python is their first, and perhaps only, programming language) Cons: * doesn't read like pseudocode (you need to interpret an arbitrary non-arithmetic symbol) * invites the question "Why doesn't this use the 'as' keyword?" * symbols are typically harder to look up than keywords * symbols don't lend themselves to easy mnemonics * somewhat arbitrary repurposing of "->" compared to its use in function annotations === Target first, ':=' symbol === while (value := read_next_item()) is not None: ... Pros: * avoids the syntactic ambiguity of "as" * being target first provides an obvious distinction from the "as" keyword * ":=" is used for name bindings in at least some other languages (but this is irrelevant to users for whom Python is their first, and perhaps only, language) Cons: * symbols are typically harder to look up than keywords * symbols don't lend themselves to easy mnemonics * subject to a visual "line noise" phenomenon when combined with other uses of ":" as a syntactic marker (e.g. slices, dict key/value pairs, lambda expressions, type annotations) === Target first, 'from' keyword === while (value from read_next_item()) is not None: # New ... Pros: * avoids the syntactic ambiguity of "as" * being target first provides an obvious distinction from the "as" keyword * typically reads nicely as pseudocode * "from" is already associated with a namebinding operation ("from module import name") Cons: * I'm sure we'll think of some more, but all I have so far is that the association with name binding is relatively weak and would need to be learned -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Sat, Apr 14, 2018 at 11:54 PM, Chris Angelico <rosuav@gmail.com> wrote:
To me, "from" strongly suggests that an element is being obtained from a container/collection of elements. This is how I conceptualize "from module import name": "name" refers to an object INSIDE the module, not the module itself. If I saw if (match from pattern.search(data)) is not None: ... I would guess that it is equivalent to m = next(pattern.search(data)) if m is not None: ... i.e. that the target is bound to the next item from an iterable (the "container"). Cheers, Nathan

On 15 April 2018 at 13:54, Chris Angelico <rosuav@gmail.com> wrote:
Ah, I forgot about that usage. The keyword usage is at least somewhat consistent, in that it's short for: _tmp = exc _exc.__cause__ from otherexc raise exc However, someone writing "raise (ExcType from otherexc)" could be confusing, since it would end up re-raising "otherexc" instead of wrapping it in a new ExcType instance. If "otherexc" was also an ExcType instance, that would be a *really* subtle bug to try and catch, so this would likely need the same kind of special casing as was proposed for "as" (i.e. prohibiting the top level parentheses). I also agree with Nathan that if you hadn't encountered from expressions before, it would be reasonable to assume they were semantically comparable to "target = next(expr)" rather than just "target = expr". Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

2018-04-15 6:08 GMT+03:00 Nick Coghlan <ncoghlan@gmail.com>:
Despite the fact that "with (cm1,cm2, cm3):" currently is the legal syntax, but as you said and as it was also noted before in this thread - it is "pointless" in 99% cases (in context of with statement) and will fail at runtime. Therefore, regardless of this PEP, maybe it is fair to make it at least to be a `SyntaxWarning` (or `SyntaxError`)? Better fail sooner than later. we should probably
show similar hesitation when it comes to giving ":" yet another meaning.
Yes, `:` is used (as a symbol) in a lot of places (in fact there is not much in it), but in some places Python looks as a combination of words and colons.
I understand that this list is subjective. But as for me it will be huge PRO that the expression comes first.
In reality, the first two points can be explained (if it will be required at all). Misunderstanding is a consequence of a lack of experience. I don't understand the the point about "visual similarity between "as" and "and" can you elaborate on this point a little bit more?
The same as previous, the expression comes first is a huge PRO for me and I'm sure for many others too. With the second point I agree that it is somewhat irrelevant.
Here I am a bit disagree with you. The most common for of assignment in formal pseudo-code is `name <- expr`. The second most common form, to my regret, is - `:=`. The `<-` form is not possible in Python and that is why `expr -> name` was suggested.
* invites the question "Why doesn't this use the 'as' keyword?"
All forms invites this question :)))
For me it is a CON. Originally the rationale of this PEP was to reduce the number of unnecessary calculations and to provide a useful syntax to make a name binding in appropriate places. It should not, in any way, replace the existing `=` usual way to make a name binding. Therefore, as I see it, it is one of design goals to make the syntax forms of `assignment statement` and `assignment expression` to be distinct and `:=` does not help with this. This does not mean that this new syntax form should not be convenient, but it should be different from the usual `=` form.
Totally agree with the last point!
As above.
* typically reads nicely as pseudocode
As for me this form implies _extraction_+binding (finding inside + binding) instead of just binding.
* "from" is already associated with a namebinding operation ("from module import name")
but module is a namespace, and `from` means _extract_ and bind.
With kind regards, -gdg

On Sun, Apr 15, 2018 at 12:19 PM, Kirill Balunov <kirillbalunov@gmail.com> wrote:
2018-04-15 6:08 GMT+03:00 Nick Coghlan <ncoghlan@gmail.com>:
[...]
Exactly, all forms invites this and other questions. First of all, coming back to original spelling choice arguments [Sorry in advance if I've missed some points in this huge thread] citation from PEP: "Differences from regular assignment statements" [...] "Otherwise, the semantics of assignment are unchanged by this proposal." So basically it's the same Python assignment? Then obvious solution seems just to propose "=". But I see Chris have put this in FAQ section: "The syntactic similarity between ``if (x == y)`` and ``if (x = y)`` ...." So IIUC, the *only* reason is to avoid '==' ad '=' similarity? If so, then it does not sound convincing at all. Of course Python does me a favor showing an error, when I make a typo like this: if (x = y) But still, if this is the only real reason, it is not convincing. Syntactically seen, I feel strong that normal '=' would be the way to go. Just look at this: y = ((eggs := spam()), (cheese := eggs.method()) y = ((eggs = spam()), (cheese = eggs.method()) The latter is so much cleaner, and already so common to any old or new Python user. And does not raise a question what this ":=" should really mean. (Or probably it should raise such question?) Given the fact that the PEP gives quite edge-case usage examples only, this should be really more convincing. And as a side note: I personally find the look of ":=" a bit 'noisy'. Another point: *Target first vs Expression first* ======================= Well, this is nice indeed. Don't you find that first of all it must be decided what should be the *overall tendency for Python*? Now we have common "x = a + b" everywhere. Then there are comprehensions (somewhat mixed direction) and "foo as bar" things. But wait, is the tendency to "give the freedom"? Then you should introduce something like "<--" in the first place so that we can write normal assignment in both directions. Or is the tendency to convert Python to the "expression first" generally? So if this question can be answered first, then I think it will be more constructive to discuss the choice of particular spellings. Mikhail

On 15 April 2018 at 19:41, Mikhail V <mikhailwas@gmail.com> wrote:
It's thoroughly convincing, because we're already familiar with the consequences of folks confusing "=" and "==" when writing C & C++ code. It's an eternal bug magnet, so it's not a design we're ever going to port over to Python. (And that's even before we get into the parsing ambiguity problems that attempting to reuse "=" for this purpose in Python would introduce, especially when it comes to keyword arguments). The only way Python will ever gain expression level name binding support is with a spelling *other than* "=", as when that's the proposed spelling, the answer will be an unequivocal "No, we're not adding that". Even if the current discussion does come up with a potentially plausible spelling, the final determination on python-dev may *still* be "No, we're not going to add that". That isn't a predetermined answer though - it will depend on whether or not a proposal can be developed that threads the small gap between "this adds too much new cognitive overhead to reading and writing the language" and "while this does add more complexity to the base language, it provides sufficient compensation in allowing common ideas to be expressed more simply".
Consider how close the second syntax is to "y = f(eggs=spam(), cheese=fromage())", though.
Given the fact that the PEP gives quite edge-case usage examples only, this should be really more convincing.
The examples in the PEP have been updated to better reflect some of the key motivating use cases (embedded assignments in if and while statement conditions, generator expressions, and container comprehensions)
And as a side note: I personally find the look of ":=" a bit 'noisy'.
You're not alone in that, which is one of the reasons finding a keyword based option that's less syntactically ambiguous than "as" could be an attractive alternative.
There's no general tendency towards expression first syntax, nor towards offering flexibility in whether ordinary assignments are target first. All the current cases where we use the "something as target" form are *not* direct equivalents to "target = something": * "import dotted.modname as name": also prevents "dotted" getting bound in the current scope the way it normally would * "from dotted import modname as name": also prevents "modname" getting bound in the current scope the way it normally would * "except exc_filter as exc": binds the caught exception, not the exception filter * "with cm as name": binds the result of __enter__ (which may be self), not the cm directly Indeed, https://www.python.org/dev/peps/pep-0343/#motivation-and-summary points out that it's this "not an ordinary assignment" aspect that lead to with statements using the "with cm as name:" structure in the first place - the original proposal in PEP 310 was for "with name = cm:" and ordinary assignment semantics. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Sun, Apr 15, 2018 at 2:01 PM, Nick Coghlan <ncoghlan@gmail.com> wrote:
Im personally "0" on the whole proposal. Just was curious about that "demonisation" of "=" and "==" visual similarity. Granted, writing ":=" instead of "=" helps a little bit. But if the ":=" will be accepted, then we end up with two spellings :-)
Keyword variants look less appealing than ":=". but if it had to be a keyword, then I'd definitely stay by "TARGET keyword EXPR" just not to swap the traditional order. Mikhail

2018-04-15 12:41 GMT+03:00 Mikhail V <mikhailwas@gmail.com>:
[OT] To be honest I never liked the fact that `=` was used in various programming languages as assignment. But it became so common that and I got used to it and even stopped taking a sedative :) So IIUC, the *only* reason is to avoid '==' ad '=' similarity?
You are not alone. On the other hand it is one of the strengths of Python - not allow to do so common and complex to finding bugs. For me personally, `: =` looks and feels just like normal assignment statement which can be used interchangeable but in many more places in the code. And if the main goal of the PEP was to offer this `assignment expression` as a future replacement for `assignment statement` the `:=` syntax form would be the very reasonable proposal (of course in this case there will be a lot more other questions). But somehow this PEP does not mean it! And with the current rationale of this PEP it's a huge CON for me that `=` and `:=` feel and look the same.
As it was noted previously `<-` would not work because of unary minus on the right:
If the idea of the whole PEP was to replace `assignment statement` with `assignment expression` I would choose name first. If the idea was to offer an expression with the name-binding side effect, which can be used in the appropriate places I would choose expression first. With kind regards, -gdg

On Sun, Apr 15, 2018 at 4:05 AM, Kirill Balunov <kirillbalunov@gmail.com> wrote:
I haven't kept up with what's in the PEP (or much of this thread), but this is the key reason I strongly prefer := as inline assignment operator.
But somehow this PEP does not mean it! And with the current rationale of this PEP it's a huge CON for me that `=` and `:=` feel and look the same.
Then maybe the PEP needs to be updated. -- --Guido van Rossum (python.org/~guido)

On Mon, Apr 16, 2018 at 3:19 AM, Guido van Rossum <guido@python.org> wrote:
I can never be sure what people are reading when they say "current" with PEPs like this. The text gets updated fairly frequently. As of time of posting, here's the rationale: ----- Naming the result of an expression is an important part of programming, allowing a descriptive name to be used in place of a longer expression, and permitting reuse. Currently, this feature is available only in statement form, making it unavailable in list comprehensions and other expression contexts. Merely introducing a way to assign as an expression would create bizarre edge cases around comprehensions, though, and to avoid the worst of the confusions, we change the definition of comprehensions, causing some edge cases to be interpreted differently, but maintaining the existing behaviour in the majority of situations. ----- Kirill, is this what you read, and if so, how does that make ':=' a negative? The rationale says "hey, see this really good thing you can do as a statement? Let's do it as an expression too", so the parallel should be a good thing. ChrisA

[Guido] 2018-04-15 20:19 GMT+03:00 Guido van Rossum <guido@python.org>:
[Chris] 2018-04-15 23:28 GMT+03:00 Chris Angelico <rosuav@gmail.com>:
Yes, this is what I read. I understand why you have such a question so I'll try to explain my position in more detail. Also I want to add that I did not fully understand about which part Guido said - "Then maybe the PEP needs to be updated." Therefore, I allow myself to assume that he had in mind the following - "The assignment expression should be semantically equivalent to assignment statement and perceived as a theoretically possible future replacement (usage) of assignment statement." If this is really the case and I understood correctly - I will repeat that for me the current state of the PEP does not fully imply this. 1. Part - "Then maybe the PEP needs to be updated." If you really see it as a theoretical substitute for assignment statement in future Python. I will update the rationale with maybe the following (I immediately warn you that I do not pretend to have a good English style): ----- Naming the result of an expression is an important part of programming, allowing a descriptive name to be used in place of a longer expression, and permitting reuse. Currently, in Python this feature is available only in statement form, making it unavailable in list comprehensions and other expression contexts. This restriction, of making it as a statement, was done primarily to avoid the usual trap of `=` vs `==` in C/C++ language. Despite this, it is evident that the ability to assign a name within an expression is convenient, allows to avoid redundant recalculations of the same expression and is a familiar feature from other programming languages. Thus the main aim of this PEP is to provide a syntax which will allow to assign as an expression and be semantically and visually interchangeable with the assignment statement. ... ----- In this case, I do not see any reason to discuss the alternative syntax - there is really only one choice `:=`. And then for me the list of open questions would look like (for example): 1. ...Is it worth accepting? 2. ...Should the other forms (+=, *=, basically all) that can not be confused with `==` be changed to expressions? ... 2. Part - How do I understand the current state of the PEP I perceive the current rationale as "hey, see this really good thing you can do as a statement? Let's do it as an expression too". Which for me means opportunities to discuss the following questions: 1. Should assignment expression be viewed as a replacement of an assignment statement or as a complement to it? 2. Which spelling should have an assignment expression? ( depends on the previous ) 3. Does it make sense to make it valid only in certain context or in any? ... and many others ... You are the author of this PEP. Therefore, you choose how you take it, how you feel it and what kind of feedback you are willing to accept, and in any case I will respect your choice :) with kind regards, -gdg

On Sun, Apr 15, 2018 at 7:19 PM, Kirill Balunov <kirillbalunov@gmail.com> wrote:
I don't think we're ever going to unify everyone on an arbitrary question of "expression first" or "name first". But to all the "expression first" people, a question: what if the target is not just a simple name? while (read_next_item() -> items[i + 1 -> i]) is not None: print("%d/%d..." % (i, len(items)), end="\r") Does this make sense? With the target coming first, it perfectly parallels the existing form of assignment:
The unpacking syntax is a bit messy, but with expression assignment, we can do this:
Okay, it's not quite as simple as C's "items[i++]" (since you have to start i off at negative one so you can pre-increment), but it's still logical and sane. Are you as happy with that sort of complex expression coming after 'as' or '->'? Not a rhetorical question. I'm genuinely curious as to whether people are expecting "expression -> NAME" or "expression -> TARGET", where TARGET can be any valid assignment target. ChrisA

2018-04-15 15:21 GMT+03:00 Chris Angelico <rosuav@gmail.com>:
I completely agree with you that it is impossible to unify everyone opinion - we all have different background. But this example is more likely to play against this PEP. This is an extra complexity within one line and it can fail hard in at least three obvious places :) And I am against this usage no matter `name first` or `expression first`. But i will reask this with following snippets. What do you choose from this examples: 0. while (items[i := i+1] := read_next_item()) is not None: print(r'%d/%d' % (i, len(items)), end='\r') 1. while (read_next_item() -> items[(i+1) -> i]) is not None: print(r'%d/%d' % (i, len(items)), end='\r') 2. while (item := read_next_item()) is not None: items[i := (i+1)] = item print(r'%d/%d' % (i, len(items)), end='\r') 3. while (read_next_item() -> item) is not None: items[(i+1) -> i] = item print(r'%d/%d' % (i, len(items)), end='\r') 4. while (item := read_next_item()) is not None: i = i+1 items[i] = item print(r'%d/%d' % (i, len(items)), end='\r') 5. while (read_next_item() -> item) is not None: i = i+1 items[i] = item print(r'%d/%d' % (i, len(items)), end='\r') I am definitely Ok with both 2 and 3 here. But as it was noted `:=` produces additional noise in other places and I am also an `expression first` guy :) So I still prefer variant 3 to 2. But to be completely honest, I would write it in the following way: for item in iter(read_next_item, None): items.append(item) print(r'%d/%d' % (i, len(items)), end='\r') With kind regards, -gdg

2018-04-15 17:17 GMT+03:00 Kirill Balunov <kirillbalunov@gmail.com>:
Oh, I forgot about `i`: for item in iter(read_next_item, None): i += 1 items.append(item) print(r'%d/%d' % (i, len(items)), end='\r') With kind regards, -gdg

On Mon, Apr 16, 2018 at 12:17 AM, Kirill Balunov <kirillbalunov@gmail.com> wrote:
These two are matching what I wrote, and are thus the two forms under consideration. I notice that you added parentheses to the second one; is there a clarity problem here and you're unsure whether "i + 1 -> i" would capture "i + 1" or "1"? If so, that's a downside to the proposal.
All of these are fundamentally different from what I'm asking: they are NOT all expressions that can be used in the while header. So it doesn't answer the question of "expression first" or "target first". Once the expression gets broken out like this, you're right back to using "expression -> NAME" or "NAME := expression", and it's the same sort of simple example that people have been discussing all along.
And that's semantically different in several ways. Not exactly a fair comparison. I invite you to write up a better example with a complex target. ChrisA

2018-04-15 23:22 GMT+03:00 Chris Angelico <rosuav@gmail.com>:
Yes parentheses were used only for clarity. I agree that I misunderstood the purpose of your question. I have no problem if the right part is a complex target, but maybe my perception is biased. With kind regards, -gdg

On Sun, Apr 15, 2018 at 10:21:02PM +1000, Chris Angelico wrote:
I don't see why it would make a difference. It doesn't to me.
Does this make sense? With the target coming first, it perfectly parallels the existing form of assignment:
Yes, except this isn't ordinary assignment-as-a-statement. I've been mulling over the question why I think the expression needs to come first here, whereas I'm satisfied with the target coming first for assignment statements, and I think I've finally got the words to explain it. It is not just long familiarity with maths and languages that put the variable first (although that's also part of it). It has to do with what we're looking for when we read code, specifically what is the primary piece of information we're initially looking for. In assignment STATEMENTS the primary piece of information is the target. Yes, of course the value assigned to the target is important, but often we don't care what the value is, at least not at first. We're hunting for a known target, and only when we find it do we care about the value it gets. A typical scenario: I'm reading a function, and I scan down the block looking at the start of each line until I find the variable I want: spam = don't care eggs = don't care self.method(don't care) cheese = ... <<<==== HERE IT IS so it actually helps to have the name up front. Copying standard maths notation for assignment (variable first, value second) is a good thing for statements. With assignment-statements, if you're scanning the code for a variable name, you're necessarily interested in the name and it will be helpful to have it on the left. But with assignment-expressions, there's an additional circumstance: sometimes you don't care about the name, you only care what the value is. (I expect this will be more common.) The name is just something to skip over when you're scanning the code looking for the value. # what did I pass as the fifth argument to the function? result = some_func(don't care, spam := don't care, eggs := don't care, self.method(don't care), cheese := HERE IT IS, ...) Of course it's hard counting commas so it's probably better to add a bit of structure to your function call: result = some_func(don't care, spam := don't care, eggs := don't care, self.method(don't care), cheese := HERE IT IS, ...) But this time we don't care about the name. Its the value we care about: result = some_func(don't care, don't care -> don't care don't care -> don't care don't care(don't care), HERE IT IS .... , ...) The target is just one more thing you have to ignore, and it is helpful to have expression first and the target second. Some more examples: # what am I adding to the total? total += don't care := expression # what key am I looking up? print(mapping[don't care := key]) # how many items did I just skip? self.skip(don't care := obj.start + extra) versus total += expression -> don't care print(mapping[key -> don't care]) self.skip(obj.start + extra -> don't care) It is appropriate for assignment statements and expressions to be written differently because they are used differently. [...]
I don't know why you would write that instead of: items = [None]*10 for i in range(3): items[i] = input("> ") or even for that matter: items = [input("> ") for i in range(3)] + [None]*7 but whatever floats your boat. (Python isn't just not Java. It's also not C *wink*)
Are you as happy with that sort of complex expression coming after 'as' or '->'?
Sure. Ignoring the output of the calls to input(): items = [None] * 10 i = -1 items[i + 1 -> i] = input("> ") items[i + 1 -> i] = input("> ") items[i + 1 -> i] = input("> ") which isn't really such a complex target. How about this instead? obj = SimpleNamespace(spam=None, eggs=None, aardvark={'key': [None, None, -1]} ) items[obj.aardvark['key'][2] + 1 -> obj.aardvark['key'][2]] = input("> ") versus: items[obj.aardvark['key'][2] := obj.aardvark['key'][2] + 1] = input("> ") Neither is exactly a shining exemplar of great code designed for readability. But putting the target on the right doesn't make it worse. -- Steve

2018-04-15 18:58 GMT+03:00 Steven D'Aprano <steve@pearwood.info>:
This made my day! :) The programming style when you absolutely don't care :))) I understand that this is a typo but it turned out to be very funny. In general, I agree with everything you've said. And I think you found a very correct way to explain why expression should go first in assignment expression. With kind regards, -gdg

On Mon, Apr 16, 2018 at 1:58 AM, Steven D'Aprano <steve@pearwood.info> wrote:
Okay, that's good. I just hear people saying "name" a lot, but that would imply restricting the grammar to just a name, and I don't know how comfortable people are with more complex targets.
It is appropriate for assignment statements and expressions to be written differently because they are used differently.
I don't know that assignment expressions are inherently going to be used in ways where you ignore the assignment part and care only about the expression part. And I disagree that assignment statements are used primarily the way you say. Frequently I skim down a column of assignments, caring primarily about the functions being called, and looking at the part before the equals sign only when I come across a parameter in another call; the important part of the line is what it's doing, not where it's stashing its result.
You and Kirill have both fallen into the trap of taking the example too far. By completely rewriting it, you destroy its value as an example. Write me a better example of a complex target if you like, but the question is about how you feel about complex assignment targets, NOT how you go about creating a particular list in memory. That part is utterly irrelevant.
The calls to input were in a while loop's header for a reason. Ignoring them is ignoring the point of assignment expressions. ChrisA

On Mon, Apr 16, 2018 at 06:16:46AM +1000, Chris Angelico wrote: [...]
Chris, I must admit that I'm utterly perplexed at this. Your example is as far as from a complex assignment target as you can possibly get. It's a simple name! i := i + 1 The target is just "i", a name. The point I was making is that your example is not a good showcase for this suggested functionality. Your code violates DRY, repeating the exact same line three times. It ought to be put in a loop, and once put in a loop, the justification for needing assignment-expression disappears. But having said that, I did respond to your question and swapping the order around: items[i + 1 -> i] = input("> ") It's still not a "complex target", the target is still just a plain ol' name, but it is precisely equivalent to your example. And then I went further and re-wrote your example to use a genuinely complex target, which I won't repeat here.
What while loop? Your example has no while loop. But regardless, we don't need to care about the *output* (i.e. your keypresses echoed to stdout) when looking at the code sample. -- Steve

On Tue, Apr 17, 2018 at 1:54 AM, Steven D'Aprano <steve@pearwood.info> wrote:
Thanks so much for the aggressively-trimmed quote. For once, though, TOO aggressive. What you're focusing on is the *unrolled* version of a two-line loop. Look at the actual loop, please, and respond to the actual question. :|
Not after it got trimmed, no. Here's what I actually said in my original post: while (read_next_item() -> items[i + 1 -> i]) is not None: print("%d/%d..." % (i, len(items)), end="\r") Now, if THAT is your assignment target, are you still as happy as you had been, or are you assuming that the target is a simple name? ChrisA

On Tue, Apr 17, 2018 at 03:36:34AM +1000, Chris Angelico wrote: [further aggressive snippage] *wink*
Ah, I never even picked up on the idea that the previous while loop was connected to the following bunch of calls to input(). The code didn't seem to be related: the first was already using -> syntax and did not use input(), the second used := syntax and did.
I'll give the answer I would have given before I read Nick's comments over on Python-Dev: sure, I'm happy, and no, I'm not assuming the target is a simple name. But having seen Nick's response on Python-Dev, I'm now wondering whether this should be limited to simple names. Further discussion in reply to Nick's post over on Python-Dev please. -- Steve

On 2018-04-15 08:58, Steven D'Aprano wrote:
Interesting. I think your arguments are pretty reasonable overall. But, for me, they just don't outweigh the fact that "->" is an ugly assignment operator that looks nothing like the existing one, whereas ":=" is a less-ugly one that has the additional benefit of looking like the existing one. From your arguments I am convinced that putting the expression first has some advantages, but they just don't seem as important to me as they apparently do to you. -- Brendan Barnwell "Do not follow where the path may lead. Go, instead, where there is no path, and leave a trail." --author unknown

On Sun, Apr 15, 2018 at 6:58 PM, Steven D'Aprano <steve@pearwood.info> wrote:
... [SNIP] ....
It is appropriate for assignment statements and expressions to be written differently because they are used differently.
Wow. That feeling when you see someone giving reasonable arguments but in the end comes up with such doubtful conclusions. So you agree that in general you may need to spot values and in other case function calls or expressions. And that's it, its just depends. So if you swap the order and in some _single_ particular case you may notice tiny advantage, you conclude that the whole case with expression assignment needs this order. Lets just return to some of proposed examples (I use "=" in both examples to be less biased here): 1. if ( match = re.match("foo", S) ) == True: print("match:", match) 2. if ( re.match("foo", S) = match ) == True: print("match:", match) Now seriously, you may argue around those "pronounce" theoretical bla bla, like "take the result and save it in a token". But the variant 1. is just better, because _it is what it is in Python_. So it is better not because it is better looking or whatever, it is same sh** turned around. So just don't turn it around! Here the 1st variant can be unwrapped to: match = re.match("foo", S) if match == True: print("match:", match) Do you see what I mean? When I read code I don't have all those things you describe in a millisecond : - look at the pointy end of operator - think, oh this shows to the right - seems like I save the value there - yep, that's the way I imply things to work - stroking the belly .... Instead I just parse visually some smaller parts of code and it's just better if the assignment is in the same order as everywhere. Yes, in some single case one order can look better, but in this case it's just not good to mix those. Mikhail

On Mon, Apr 16, 2018 at 11:05 PM, Mikhail V <mikhailwas@gmail.com> wrote:
You start by attempting to be less biased, but you're using existing Python syntax and then justifying one of the two options because it's existing Python syntax. I'm not sure that that's a strong argument :)
So it is better not because it is better looking or whatever, it is same sh** turned around. So just don't turn it around!
Obviously if the chosen token is ":=", it's going to be target first.
Here are the three most popular syntax options, and how each would be explained: 1) "target := expr" ==> It's exactly the same as other forms of assignment, only now it's an expression. 2) "expr as name" ==> It's exactly the same as other uses of "as", only now it's just grabbing the preceding expression, not actually doing anything with it 3) "expr -> name" ==> The information went data way. So either you take a parallel from elsewhere in Python syntax, or you take a hopefully-intuitive dataflow mnemonic symbol. Take your pick. ChrisA

On 4/16/18 1:42 PM, Chris Angelico wrote:
My problem with the "->" option is that function annotations already use "->" to indicate the return type of a function. This is an unfortunate parallel from elsewhere in Python syntax, since the meaning is completely different. ":=" is at least new syntax. "as" is nice in that it's already used for assignment, but seems to be causing too much difficulty in parsing, whether by compilers or people. --Ned.

On Mon, Apr 16, 2018 at 11:11 AM Ned Batchelder <ned@nedbatchelder.com> wrote:
FWIW - We used "as" in our Python C++ binding interface description language in CLIF to denote renaming from the original C++ name to a new name in Python - effectively an assignment syntax. https://github.com/google/clif/blob/master/clif/python/primer.md I currently have a "-0" opinion on the entire PEP 572 as I don't buy that assignments within expressions are even a good thing to have in the language. #complexity - Think of people learning the language. -gps

How about "name being expression" - this avoids the already used "as" while being searchable, reasonably short and gives a reasonably clear, (at least to English speakers), indication of what is going on. It can also be typed on an ASCII keyboard without having to have a helper program or memorising Unicode codes and can be displayed or printed without having to install specialised fonts. If a postfix notation is considered desirable, either instead or as well as "being", then possibly another synonym would suit such as "expression stored_as name" or "expression storedas name" (not apologies for the awkward name as I personally find it an awkward construction just like Reverse Polish). -- Steve (Gadget) Barnes Any opinions in this message are my personal opinions and do not reflect those of my employer. --- This email has been checked for viruses by AVG. http://www.avg.com

On Tue, Apr 17, 2018 at 5:11 AM, Steve Barnes <gadgetsteve@live.co.uk> wrote:
IMO searchability isn't enough of an advantage to justify creating a new keyword, which could potentially break people's code. (I don't think it'll break the stdlib, but it'll almost certainly break at least some code out there.) New keywords have an extremely high bar to reach. ChrisA

On Mon, Apr 16, 2018 at 8:42 PM, Chris Angelico <rosuav@gmail.com> wrote:
As I initially said, I just don't find this choice list fair. and it should be: 1) "target = expr" (which would be first intuitive idea from user's PoV, and if can't be made then explained to Python people why not) 2) "target := expr" (as probably best close alternative) 3) "target ? expr" (where ? is some other word/character - IIRC "target from expr" was proposed once) That's it. But well, if I compare to your choice list - there is ":=" option you have as well :)

On Tue, Apr 17, 2018 at 5:42 AM, Mikhail V <mikhailwas@gmail.com> wrote:
That one is dealt with in the PEP. It is not an option.
2) "target := expr" (as probably best close alternative)
Yep, got that one.
3) "target ? expr" (where ? is some other word/character - IIRC "target from expr" was proposed once)
... which isn't specific enough to be a front-runner option.
Yes. I'm not sure what's unfair about the options I've given there. ChrisA

[Chris]
It does give a natural solution to one of the problematic examples, because as a very-low-precedence operator it would suck up "everything to the left" instead of "everything to the right" as "the expression part":: if f(x) -> a is not None: can't be misunderstood, but: if a := f(x) is not None: is routinely misunderstood. On the other hand, if assignment expressions are expanded to support all the forms of unpacking syntax (as Guido appears to favor), then other cases arise: if f() -> a, b > (3, 6): Is that: if ((f() -> a), b) > (3, 6): or if (f() -> (a, b)) > (3, 6): ? Which is an argument in favor of ":=" to me: an assignment statement can be pretty complex, and if an assignment operator can become just as complex then it's best if it looks and works (as much as possible} exactly like an assignment statement. If someone writes if a, b := f() > (3, 6): it's easy to explain why that's broken by referring to the assignment statement a, b = f() > (3, 6) "You're trying to unpack a Boolean into a 2-tuple - add parens to express what you really want."

Before, I briefly mentioned the idea if this could be unified with except/with's "as". To the casual observer, they're really similar. However, their semantics would be totally different, and people don't seem to like a unification attempt. A huge argument against "as" would be to prevent confusion, especially for new people. I must admit I like putting the expression first, though. Even if it's just to make it harder to mix it up with normal assignment. Perhaps => could be used - it's a new token, unlike -> which is used to annotate return values, it's not legal syntax now(so no backwards compatibility issues), and used a for similar purposes in for example php when declaring associative arrays.($arr = array("key"=>"value");). I'm not convinced myself, though.
participants (24)
-
Brendan Barnwell
-
Chris Angelico
-
Clint Hepner
-
David Mertz
-
Ethan Furman
-
Evpok Padding
-
George Leslie-Waksman
-
Gregory P. Smith
-
Guido van Rossum
-
Jacco van Dorp
-
Kirill Balunov
-
Michel Desmoulin
-
Mike Miller
-
Mikhail V
-
Nathan Schneider
-
Ned Batchelder
-
Nick Coghlan
-
Nikolaus Rath
-
Paul Moore
-
Peter O'Connor
-
Stephen J. Turnbull
-
Steve Barnes
-
Steven D'Aprano
-
Tim Peters