From 6d81538ecb9592ed14aa5bdeadf1502dda7cd0d3 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Tue, 22 May 2018 12:49:00 -0700 Subject: [PATCH] Pep 572 update (#654) - Remove changes to comprehension scope - Make := in comprehensions assign to containing non-comprehension scope - Clarify binding precedence (tighter than comma, not at same level as =) - Remove mention of more complex targets in the future - Explicitly disallow toplevel := - Rewrite section on differences with =, enumerating all of them - Remove "here's how this could be written without :=" from examples - Tweak first paragraph of "Syntax and semantics" - Add "Exception cases" (various explicit prohibitions) - Clarify that lambda is a containing scope - Clarify that := and = just don't mix - Added "Open questions" section - Added two new rejected alternatives: "Allowing commas to the right" and "Always requiring parentheses" - Minor edits * Start a section on real code * Correct/clarify "commas to the right" section * Add Tim and Guido as authors * Update abstract to mention := * Rule out targets conflicting with comprehension loop control * Add timcode.txt as Appendix A * Add os.fork() example * Add TODOs about evaluation order --- pep-0572.rst | 788 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 542 insertions(+), 246 deletions(-) diff --git a/pep-0572.rst b/pep-0572.rst index 643532824cc..ced2a413a71 100644 --- a/pep-0572.rst +++ b/pep-0572.rst @@ -1,6 +1,7 @@ PEP: 572 Title: Assignment Expressions -Author: Chris Angelico +Author: Chris Angelico , Tim Peters , + Guido van Rossum Status: Draft Type: Standards Track Content-Type: text/x-rst @@ -14,8 +15,7 @@ Abstract ======== This is a proposal for creating a way to assign to variables within an -expression. Additionally, the precise scope of comprehensions is adjusted, to -maintain consistency and follow expectations. +expression using the notation ``NAME := expr``. Rationale @@ -25,11 +25,7 @@ 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. +expression contexts. Additionally, naming sub-parts of a large expression can assist an interactive debugger, providing useful display hooks and partial results. Without a way to @@ -39,13 +35,83 @@ code; with assignment expressions, this merely requires the insertion of a few the code be inadvertently changed as part of debugging (a common cause of Heisenbugs), and is easier to dictate to another programmer. +The importance of real code +--------------------------- + +During the development of this PEP many people (supporters and critics +both) have had a tendency to focus on toy examples on the one hand, +and on overly complex examples on the other. + +The danger of toy examples is twofold: they are often too abstract to +make anyone go "ooh, that's compelling", and they are easily refuted +with "I would never write it that way anyway". + +The danger of overly complex examples is that they provide a +convenient strawman for critics of the proposal to shoot down ("that's +obfuscated"). + +Yet there is some use for both extremely simple and extremely complex +examples: they are helpful to clarify the intended semantics. +Therefore there will be some of each below. + +However, in order to be *compelling*, examples should be rooted in +real code, i.e. code that was written without any thought of this PEP, +as part of a useful application, however large or small. Tim Peters +has been extremely helpful by going over his own personal code +repository and picking examples of code he had written that (in his +view) would have been *clearer* if rewritten with (sparing) use of +assignment expressions. His conclusion: the current proposal would +have allowed a modest but clear improvement in quite a few bits of +code. + +Another use of real code is to observe indirectly how much value +programmers place on compactness. Guido van Rossum searched through a +Dropbox code base and discovered some evidence that programmers value +writing fewer lines over shorter lines. + +Case in point: Guido found several examples where a programmer +repeated a subexpression, slowing down the program, in order to save +one line of code, e.g. instead of writing:: + + match = re.match(data) + group = match.group(1) if match else None + +they would write:: + + group = re.match(data).group(1) if re.match(data) else None + +Another example illustrates that programmers sometimes do more work to +save an extra level of indentation:: + + match1 = pattern1.match(data) + match2 = pattern2.match(data) + if match1: + return match1.group(1) + elif match2: + return match2.group(2) + +This code tries to match ``pattern2`` even if ``pattern1`` has a match +(in which case the match on ``pattern2`` is never used). The more +efficient rewrite would have been:: + + match1 = pattern1.match(data) + if match1: + return match1.group(1) + else: + match2 = pattern2.match(data) + if match2: + return match2.group(2) + +(TODO: Include Guido's evidence, and do a more systematic search.) + Syntax and semantics ==================== -In any context where arbitrary Python expressions can be used, a **named -expression** can appear. This is of the form ``name := expr`` where -``expr`` is any valid Python expression, and ``name`` is an identifier. +In most contexts where arbitrary Python expressions can be used, a +**named expression** can appear. This is of the form ``NAME := expr`` +where ``expr`` is any valid Python expression other than an +unparenthesized tuple, and ``NAME`` is an identifier. The value of such a named expression is the same as the incorporated expression, with the additional side-effect that the target is assigned @@ -62,140 +128,221 @@ that value:: # 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] +Exceptional cases +----------------- + +There are a few places where assignment expressions are not allowed, +in order to avoid ambiguities or user confusion: + +- Unparenthesized assignment expressions are prohibited at the top + level of an expression statement; for example, this is not allowed:: + + y := f(x) # INVALID + + This rule is included to simplify the choice for the user between an + assignment statements and an assignment expression -- there is no + syntactic position where both are valid. + +- Unparenthesized assignment expressions are prohibited at the top + level in the right hand side of an assignment statement; for + example, the following is not allowed:: + + y0 = y1 := f(x) # INVALID + + Again, this rule is included to avoid two visually similar ways of + saying the same thing. + +- Unparenthesized assignment expressions are prohibited for the value + of a keyword argument in a call; for example, this is disallowed:: + + foo(x = y := f(x)) # INVALID + + This rule is included to disallow excessively confusing code. + +- TODO: Should we disallow using keyword arguments and top level + assignment expressions in the same call? E.g.:: + + # Should these be invalid? + foo(x=0, y := f(0)) + bar(x := 0, y = f(x)) + + Regardless, ``foo(x := 0)`` should probably be valid (see below). -Differences from regular assignment statements ----------------------------------------------- +- Assignment expressions (even parenthesized or occurring inside other + constructs) are prohibited in function default values. For example, + the following examples are all invalid, even though the expressions + for the default values are valid in other contexts:: + + def foo(answer = p := 42): # INVALID + ... + + def bar(answer = (p := 42)): # INVALID + ... + + def baz(callback = (lambda arg: p := arg)): # INVALID + ... + + This rule is included to avoid side effects in a position whose + exact semantics are already confusing to many users (cf. the common + style recommendation against mutable default values). (TODO: Maybe + this should just be a style recommendation except for the + prohibition at the top level?) + +Scope of the target +------------------- + +An assignment expression does not introduce a new scope. In most +cases the scope in which the target will be bound is self-explanatory: +it is the current scope. If this scope contains a ``nonlocal`` or +``global`` declaration for the target, the assignment expression +honors that. + +There is one special case: an assignment expression occurring in a +list, set or dict comprehension or in a generator expression (below +collectively referred to as "comprehensions") binds the target in the +containing scope, honoring a ``nonlocal`` or ``global`` declaration +for the target in that scope, if one exists. For the purpose of this +rule the containing scope of a nested comprehension is the scope that +contains the outermost comprehension. A lambda counts as a containing +scope. + +The motivation for this special case is twofold. First, it allows us +to conveniently capture a "witness" for an ``any()`` expression, or a +counterexample for ``all()``, for example:: + + if any((comment := line).startswith('#') for line in lines): + print("First comment:", comment) + else: + print("There are no comments") + + if all((nonblank := line).strip() == '' for line in lines): + print("All lines are blank") + else: + print("First non-blank line:", nonblank) + +Second, it allows a compact way of updating mutable state from a +comprehension, for example:: + + # Compute partial sums in a list comprehension + total = 0 + partial_sums = [total := total + v for v in values] + print("Total:", total) + +An exception to this special case applies when the target name is the +same as a loop control variable for a comprehension containing it. +This is invalid. (This exception exists to rule out edge cases of the +above scope rules as illustrated by ``[i := i+1 for i in range(5)]`` +or ``[[(j := j) for i in range(5)] for j in range(5)]``. Note that +this exception also applies to ``[i := 0 for i, j in stuff]``.) + +A further exception applies when an assignment expression occurrs in a +comprehension whose containing scope is a class scope. If the rules +above were to result in the target being assigned in that class's +scope, the assignment expression is expressly invalid. + +(The reason for the latter exception is the implicit function created +for comprehensions -- there is currently no runtime mechanism for a +function to refer to a variable in the containing class scope, and we +do not want to add such a mechanism. If this issue ever gets resolved +this special case may be removed from the specification of assignment +expressions. Note that the problem already exists for *using* a +variable defined in the class scope from a comprehension.) + +Relative precedence of ``:=`` +----------------------------- + +The ``:=`` operator groups more tightly than a comma in all syntactic +positions where it is legal, but less tightly than all operators, +including ``or``, ``and`` and ``not``. As follows from section +"Exceptional cases" above, it is never allowed at the same level as +``=``. In case a different grouping is desired, parentheses should be +used. + +The ``:=`` operator may be used directly in a positional function call +argument; however it is invalid directly in a keyword argument. + +Some examples to clarify what's technically valid or invalid:: + + # INVALID + x := 0 + + # Valid alternative + (x := 0) + + # INVALID + x = y := 0 + + # Valid alternative + x = (y := 0) + + # Valid + len(lines := f.readlines()) + + # Valid (TODO: Should this be disallowed?) + foo(x := 3, cat='vector') + + # INVALID + foo(cat=category := 'vector') + + # Valid alternative + foo(cat=(category := 'vector')) + +Most of the "valid" examples above are not recommended, since human +readers of Python source code who are quickly glancing at some code +may miss the distinction. But simple cases are not objectionable:: + + # Valid + if any(len(longline := line) >= 100 for line in lines): + print("Extremely long line:", longline) + +This PEP recommends always putting spaces around ``:=``, similar to +PEP 8's recommendation for ``=`` when used for assignment, whereas the +latter disallows spaces around ``=`` used for keyword arguments.) + + +Differences between assignment expressions and assignment statements +--------------------------------------------------------------------- Most importantly, since ``:=`` is an expression, it can be used in contexts where statements are illegal, including lambda functions and comprehensions. -An assignment statement can assign to multiple targets, left-to-right:: +Conversely, assignment expressions don't support the advanced features +found in assignment statements: - x = y = z = 0 +- Multiple targets are not directly supported:: -The equivalent assignment expression is parsed as separate binary operators, -and is therefore processed right-to-left, as if it were spelled thus:: + x = y = z = 0 # Equivalent: (x := (y := (z := 0))) - assert 0 == (x := (y := (z := 0))) +- Single assignment targets more complex than a single ``NAME`` are + not supported:: -Statement assignment can include annotations. This would be syntactically -noisy in expressions, and is of minor importance. An annotation can be -given separately from the assignment if needed:: + # No equivalent + a[i] = x + self.rest = [] - x:str = "" # works - (x:str := "") # SyntaxError - x:str # possibly before a loop - (x := "") # fine +- Iterable packing and unpacking (both regular or extended forms) are + not supported:: -Augmented assignment is not supported in expression form:: + # Equivalent needs extra parentheses + loc = x, y # Use (loc := (x, y)) + info = name, phone, *rest # Use (info := (name, phone, *rest)) - >>> x +:= 1 - File "", line 1 - x +:= 1 - ^ - SyntaxError: invalid syntax + # No equivalent + px, py, pz = position + name, phone, email, *other_info = contact -Statement assignment is able to set attributes and subscripts, but -expression assignment is restricted to names. (This restriction may be -relaxed in a future version of Python.) +- Type annotations are not supported:: -Otherwise, the semantics of assignment are identical in statement and -expression forms. + # No equivalent + p: Optional[int] = None +- Augmented assignment is not supported:: -Alterations to comprehensions ------------------------------ + total += tax # Equivalent: (total := total + tax) -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 (iterator): - result = [] - for x in iterator: - for y in range(4): - result.append(x + y) - return result - numbers = (iter(range(3))) - -Under the new semantics, this would instead be equivalent to:: - - def (): - result = [] - for x in range(3): - for y in range(4): - result.append(x + y) - return result - numbers = () - -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 (iterator): - result = [] - for name in iterator: - result.append(prefix + name) - return result - prefixed_names = (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; and the -same early binding then handles the outermost iterable as well. The list -comprehension is thus approximately equivalent to:: - - class X: - names = ["Fred", "Barney", "Joe"] - prefix = "> " - def (names=names, prefix=prefix): - result = [] - for name in names: - result.append(prefix + name) - return result - prefixed_names = () - -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. - - -Recommended use-cases -===================== + +Examples +======== Simplifying list comprehensions ------------------------------- @@ -210,21 +357,8 @@ giving it a name on first use:: stuff = [[y := f(x), x/y] for x in range(5)] - # There are a number of less obvious ways to spell this in current - # versions of Python, such as: - - # 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)]] - - # 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)] - -In all cases, the name is local to the comprehension; like iteration variables, -it cannot leak out into the surrounding context. +Note that in both cases the variable ``y`` is bound in the containing +scope (i.e. at the same level as ``results`` or ``stuff``). Capturing condition values @@ -233,7 +367,7 @@ Capturing condition values Assignment expressions can be used to good effect in the header of an ``if`` or ``while`` statement:: - # Proposed syntax + # Loop-and-a-half while (command := input("> ")) != "quit": print("You entered:", command) @@ -250,26 +384,64 @@ an ``if`` or ``while`` statement:: print("Fallback found:", match.group(0)) # Reading socket data until an empty string is returned - while data := sock.read(): + while data := sock.recv(): print("Received data:", data) - # Equivalent in current Python, not caring about function return value - while input("> ") != "quit": - print("You entered a command.") - - # To capture the return value in current Python demands a four-line - # loop header. - while True: - command = input("> "); - if command == "quit": - break - print("You entered:", command) - 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. +Fork +---- + +An example from the low-level UNIX world:: + + if pid := os.fork(): + # Parent code + else: + # Child code + + +Open questions and TODOs +======================== + +- For precise semantics, the proposal requires evaluation order to be + well-defined. We're mostly good due to the rule that things + generally are evaluated from left to right, but there are some + corner cases: + + 1. In a dict comprehension ``{X: Y for ...}``, ``Y`` is evaluated + before ``X``. This is confusing and should be swapped. (In a + dict display ``{X: Y}}`` the order is already ``X`` before + ``Y``.) + + 2. It would be good to confirm definitively that in an assignment + statement, any subexpressions on the left hand side are + evaluated after the right hand side (e.g. ``a[X] = Y`` evaluates + ``X`` after ``Y``). (This already seems to be the case.) + + 3. Also in multiple assignment statements (e.g. ``a[X] = a[Y] = Z``) + it would be good to confirm that ``a[X]`` is evaluated before + ``a[Y]``. (This already seems to be the case.) + +- Should we adopt Tim Peters's proposal to make the target scope be the + containing scope? It's cute, and has some useful applications, but + it requires a carefully formulated mouthful. (Current answer: yes.) + +- Should we disallow combining keyword arguments and unparenthesized + assignment expressions in the same call? (Current answer: no.) + +- Should we disallow ``(x := 0, y := 0)`` and ``foo(x := 0, y := 0)``, + requiring the fully parenthesized forms ``((x := 0), (y := 0))`` and + ``foo((x := 0), (y := 0))`` instead? (Current answer: no.) + +- If we were to change the previous answer to yes, should we still + allow ``len(lines := f.readlines())``? (I'd say yes.) + +- Should we disallow assignment expressions anywhere in function + defaults? (Current answer: yes.) + Rejected alternative proposals ============================== @@ -279,6 +451,17 @@ Below are a number of alternative syntaxes, some of them specific to comprehensions, which have been rejected in favour of the one given above. +Changing the scope rules for comprehensions +------------------------------------------- + +A previous version of this PEP proposed subtle changes to the scope +rules for comprehensions, to make them more usable in class scope and +to unify the scope of the "outermost iterable" and the rest of the +comprehension. However, this part of the proposal would have caused +backwards incompatibilities, and has been withdrawn so the PEP can +focus on assignment expressions. + + Alternative spellings --------------------- @@ -426,102 +609,46 @@ Once find() returns -1, the loop terminates. If ``:=`` binds as loosely as While this behaviour would be convenient in many situations, it is also harder to explain than "the := operator behaves just like the assignment statement", and as such, the precedence for ``:=`` has been made as close as possible to -that of ``=``. - - -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 now are not, and a few corner cases -with altered semantics. - +that of ``=`` (with the exception that it binds tighter than comma). -The Outermost Iterable ----------------------- - -As of Python 3.7, the outermost iterable in a comprehension is special: it is -evaluated in the surrounding context, instead of inside the comprehension. -Thus it is permitted to contain a ``yield`` expression, to use a name also -used elsewhere, and to reference names from class scope. Also, in a genexp, -the outermost iterable is pre-evaluated, but the rest of the code is not -touched until the genexp is first iterated over. Class scope is now handled -more generally (see above), but if other changes require the old behaviour, -the iterable must be explicitly elevated from the comprehension:: - - # Python 3.7 - def f(x): - return [x for x in x if x] - def g(): - return [x for x in [(yield 1)]] - # With PEP 572 - def f(x): - return [y for y in x if y] - 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. - -The following expressions would, in Python 3.7, raise exceptions immediately. -With the removal of the outermost iterable's special casing, they are now -equivalent to the most obvious longhand form:: - - 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)) # ZeroDivisionError - - def (): - for x in rage(10): - yield x - gen = () # No exception yet - tng = next(gen) # NameError +Allowing commas to the right +---------------------------- -Open questions -============== +Some critics have claimed that the assignment expressions should allow +unparenthesized tuples on the right, so that these two would be equivalent:: -Importing names into comprehensions ------------------------------------ + (point := (x, y)) + (point := x, y) -A list comprehension can use and update local names, and they will retain -their values from one iteration to another. It would be convenient to use -this feature to create rolling or self-effecting data streams:: +(With the current version of the proposal, the latter would be +equivalent to ``((point := x), y)``.) - progressive_sums = [total := total + value for value in data] +However, adopting this stance would logically lead to the conclusion +that when used in a function call, assignment expressions also bind +less tight than comma, so we'd have the following confusing equivalence:: -This will fail with UnboundLocalError due to ``total`` not being initalized. -Simply initializing it outside of the comprehension is insufficient - unless -the comprehension is in class scope:: + foo(x := 1, y) + foo(x := (1, y)) - class X: - total = 0 - progressive_sums = [total := total + value for value in data] +The less confusing option is to make ``:=`` bind more tightly than comma. -At other scopes, it may be beneficial to have a way to fetch a value from the -surrounding scope. Should this be automatic? Should it be controlled with a -keyword? Hypothetically (and using no new keywords), this could be written:: - total = 0 - progressive_sums = [total := total + value - import nonlocal total - for value in data] +Always requiring parentheses +---------------------------- -Translated into longhand, this would become:: +It's been proposed to just always require parenthesize around an +assignment expression. This would resolve many ambiguities, and +indeed parentheses will frequently be needed to extract the desired +subexpression. But in the following cases the extra parentheses feel +redundant:: - total = 0 - def (total=total): - result = [] - for value in data: - result.append(total := total + value) - return result - progressive_sums = () + # Top level in if + if match := pattern.match(line): + return match.group(1) -ie utilizing the same early-binding technique that is used at class scope. + # Short call + len(lines := f.readlines()) Frequently Raised Objections @@ -565,10 +692,6 @@ create externally-visible names. This is no different from ``for`` loops or other constructs, and can be solved the same way: ``del`` the name once it is no longer needed, or prefix it with an underscore. -Names bound within a comprehension are local to that comprehension, even in -the outermost iterable, and can thus be used freely without polluting the -surrounding namespace. - (The author wishes to thank Guido van Rossum and Christoph Groth for their suggestions to move the proposal in this direction. [2]_) @@ -590,11 +713,184 @@ benefit of style guides such as PEP 8, two recommendations are suggested. Acknowledgements ================ -The author wishes to thank Guido van Rossum and Nick Coghlan for their -considerable contributions to this proposal, and to members of the +The authors wish to thank Nick Coghlan and Steven D'Aprano for their +considerable contributions to this proposal, and members of the core-mentorship mailing list for assistance with implementation. +Appendix A: Tim Peters's findings +================================= + +Here's a brief essay Tim Peters wrote on the topic. + +I dislike "busy" lines of code, and also dislike putting conceptually +unrelated logic on a single line. So, for example, instead of:: + + i = j = count = nerrors = 0 + +I prefer:: + + i = j = 0 + count = 0 + nerrors = 0 + +instead. So I suspected I'd find few places I'd want to use +assignment expressions. I didn't even consider them for lines already +stretching halfway across the screen. In other cases, "unrelated" +ruled:: + + mylast = mylast[1] + yield mylast[0] + +is a vast improvment over the briefer:: + + yield (mylast := mylast[1])[0] + +The original two statements are doing entirely different conceptual +things, and slamming them together is conceptually insane. + +In other cases, combining related logic made it harder to understand, +such as rewriting:: + + while True: + old = total + total += term + if old == total: + return total + term *= mx2 / (i*(i+1)) + i += 2 + +as the briefer:: + + while total != (total := total + term): + term *= mx2 / (i*(i+1)) + i += 2 + return total + +The ``while`` test there is too subtle, crucially relying on strict +left-to-right evaluation in a non-short-circuiting or method-chaining +context. My brain isn't wired that way. + +But cases like that were rare. Name binding is very frequent, and +"sparse is better than dense" does not mean "almost empty is better +than sparse". For example, I have many functions that return ``None`` +or ``0`` to communicate "I have nothing useful to return in this case, +but since that's expected often I'm not going to annoy you with an +exception". This is essentially the same as regular expression search +functions returning ``None`` when there is no match. So there was lots +of code of the form:: + + result = solution(xs, n) + if result: + # use result + +I find that clearer, and certainly a bit less typing and +pattern-matching reading, as:: + + if result := solution(xs, n): + # use result + +It's also nice to trade away a small amount of horizontal whitespace +to get another _line_ of surrounding code on screen. I didn't give +much weight to this at first, but it was so very frequent it added up, +and I soon enough became annoyed that I couldn't actually run the +briefer code. That surprised me! + +There are other cases where assignment expressions really shine. +Rather than pick another from my code, Kirill Balunov gave a lovely +example from the standard library's ``copy()`` function in ``copy.py``:: + + reductor = dispatch_table.get(cls) + if reductor: + rv = reductor(x) + else: + reductor = getattr(x, "__reduce_ex__", None) + if reductor: + rv = reductor(4) + else: + reductor = getattr(x, "__reduce__", None) + if reductor: + rv = reductor() + else: + raise Error("un(shallow)copyable object of type %s" % cls) + +The ever-increasing indentation is semantically misleading: the logic +is conceptually flat, "the first test that succeeds wins":: + + if reductor := dispatch_table.get(cls): + rv = reductor(x) + elif reductor := getattr(x, "__reduce_ex__", None): + rv = reductor(4) + elif reductor := getattr(x, "__reduce__", None): + rv = reductor() + else: + raise Error("un(shallow)copyable object of type %s" % cls) + +Using easy assignment expressions allows the visual structure of the +code to emphasize the conceptual flatness of the logic; +ever-increasing indentation obscured it. + +A smaller example from my code delighted me, both allowing to put +inherently related logic in a single line, and allowing to remove an +annoying "artificial" indentation level:: + + diff = x - x_base + if diff: + g = gcd(diff, n) + if g > 1: + return g + +became:: + + if (diff := x - x_base) and (g := gcd(diff, n)) > 1: + return g + +That ``if`` is about as long as I want my lines to get, bur remains easy +to follow. + +So, in all, in most lines binding a name, I wouldn't use assignment +expressions, but because that construct is so very frequent, that +leaves many places I would. In most of the latter, I found a small +win that adds up due to how often it occurs, and in the rest I found a +moderate to major win. I'd certainly use it more often than ternary +``if``, but significantly less often than augmented assignment. + +A numeric example +----------------- + +I have another example that quite impressed me at the time. + +Where all variables are positive integers, and a is at least as large +as the n'th root of x, this algorithm returns the floor of the n'th +root of x (and roughly doubling the number of accurate bits per +iteration):: + + while a > (d := x // a**(n-1)): + a = ((n-1)*a + d) // n + return a + +It's not obvious why that works, but is no more obvious in the "loop +and a half" form. It's hard to prove correctness without building on +the right insight (the "arithmetic mean - geometric mean inequality"), +and knowing some non-trivial things about how nested floor functions +behave. That is, the challenges are in the math, not really in the +coding. + +If you do know all that, then the assignment-expression form is easily +read as "while the current guess is too large, get a smaller guess", +where the "too large?" test and the new guess share an expensive +sub-expression. + +To my eyes, the original form is harder to understand:: + + while True: + d = x // a**(n-1) + if a <= d: + break + a = ((n-1)*a + d) // n + return a + + References ==========