October 14, 2019·6 min read·engineeringpythonlanguageapi

Python 3.8 makes assignment results available inside expressions

Assignment expressions let a condition retain the value it tests. Their usefulness depends on evaluation order, scope and keeping the binding easy to interpret.

A regular-expression match often needs to be tested before its captured values can be used. Without a binding inside the condition, code has to name the result in a preceding statement or repeat the search. Python 3.8 adds assignment expressions so the condition can compute a value once, retain it and decide whether to enter the body.

Python 3.8.0 is the stable release published on October 14, 2019. Assignment expressions are its most visible syntax change, but the useful question is what they let a program express. The new operator connects a value's computation to a nearby decision that needs both the value and a test of it.

The release-tagged explanation uses regular expressions, repeated length calculations and block-reading loops as examples. These are existing patterns with a repeated dependency. A condition needs a computed value, and the body needs that same value after the condition succeeds.

I regard the change as useful when it removes duplicated work without making the reader reconstruct evaluation order. Python's compact expressions already require attention to scope and truth testing. Adding a binding is worthwhile when the relationship between the test and the value becomes easier to follow.

A named expression returns the assigned value

PEP 572 specifies the form as a name followed by := and an expression. The expression evaluates, its value is assigned to the name, and that same value becomes the result of the assignment expression. The target is an identifier, which is a narrower form than the full range of assignment statements.

A regular-expression example shows the intended relationship. The result has a name before the condition chooses whether to execute the body, so the body can use the search's captured information.

import re

text = "discount=15"
if (match := re.search(r"discount=(\d+)", text)) is not None:
    percentage = int(match.group(1))
    print(percentage)

The search executes once. Its result is bound to match, and the condition compares that result with None. The body can then use the captured digits through the same match object. The code does not perform a second search to obtain the value it has already tested.

The explicit comparison also separates the binding from its condition. In other examples, the result's truth value can supply the condition directly. Here, the test states which result means there was no match. The assignment expression itself does not impose a particular success predicate.

A plain assignment before the if remains a valid way to express the same dependency. The new syntax adds an option where the binding belongs naturally to the condition. It does not make a preceding assignment incorrect or require every local variable to move into an expression.

Loop termination can retain the final read result

A block-reading loop has the same structure. It needs to obtain a block, test whether reading has reached the end, and pass a nonempty block to processing code. Without an assignment expression, these concerns can require a separate read before the loop and another at its end, or an unconditional loop with a break.

Python 3.8 permits the binding directly in the loop condition. The following function accumulates the lengths of blocks until its input stream returns an empty result.

def count_bytes(stream):
    total = 0
    while block := stream.read(8192):
        total += len(block)
    return total

This function expects a binary stream whose read method returns empty bytes at end of input. Every successful condition supplies the block used by the body. The expression is evaluated again before the next iteration, so the code contains one place where a read occurs.

That organization matters if the read parameters change. A duplicated read can drift when only one occurrence receives an edit. Keeping it in a single condition removes that particular maintenance risk. The benefit comes from consolidating an operation with a simple termination rule, not from minimizing lines as an end in itself.

There are still cases where an explicit loop body is clearer. If the code needs multiple steps between obtaining a value and deciding whether to continue, forcing those steps into a condition can obscure them. The release documentation recommends limiting the operator to cases where it reduces complexity and improves readability.

Parentheses and precedence carry meaning

The operator's spelling deliberately differs from the ordinary assignment statement. PEP 572 also restricts its unparenthesized use in several contexts. For example, a bare assignment expression cannot simply replace an assignment statement at the top level. These restrictions reduce ambiguity between the existing statement and the new expression.

Grouping is especially important when a computed value participates in another comparison. The release documentation's length example places parentheses around the binding before comparing the retained length with a limit. That tells the reader which value is assigned and which expression supplies the condition.

An author should therefore inspect the bound value, not merely whether the condition succeeds. A condition that retains a Boolean comparison result is a different program from one that retains the object being compared. Both may produce a truth value usable by if, but only one preserves the object the body needs.

The syntax also interacts with f-string formatting. PEP 572 requires parentheses for assignment expressions inside f-strings because a colon already starts a format specification there. This preserves the existing interpretation of formatting syntax. A visually similar sequence of punctuation does not necessarily denote a binding without the required grouping.

Comprehensions have a deliberate scope rule

An assignment expression does not create a new scope. In ordinary code it binds in the current scope and respects an applicable global or nonlocal declaration. That makes the new syntax a way to bind a name within existing scope rules, rather than a new kind of local block.

Comprehensions require more care. PEP 572 says a named expression inside a comprehension binds in the containing scope, with the corresponding handling of global and nonlocal. Its rationale includes retaining a value that demonstrates why an any() call succeeded or why an all() call failed.

That retained name is different from the comprehension's iteration variable. The proposal forbids an assignment-expression target from reusing a loop-target name in a containing comprehension. Otherwise the same name would have contradictory requirements about which scope owns it. The restriction is a semantic boundary that a short example can conceal.

For application code, the practical consequence is that a binding inside a comprehension can remain relevant outside that comprehension. A reader cannot assume every name appearing there is confined to the iteration. Choosing a descriptive target matters because the name may become part of the surrounding function's state.

The proposal also changes dictionary-comprehension evaluation order so the key is evaluated before the value. This lets a key calculation bind a name that the value calculation reuses. The order is part of how assignment expressions become useful in that context, rather than an unrelated formatting detail.

The proposal's motivation includes examples drawn from existing code. It discusses repeated regular-expression work and extra indentation needed to delay a second operation until a first attempt fails. Those examples explain why retaining a computed value matters. A shorter expression is useful when it preserves that dependency without evaluating work whose result the program will never use.

Positional-only signatures make another contract explicit

The release also adds slash syntax for positional-only parameters. Its purpose is different from assignment expressions, but both changes make a previously awkward relationship directly expressible. A function can now state that callers must supply some arguments by position, leaving their local parameter names available for later changes.

The release documentation shows that parameters before the slash can coexist with ordinary parameters and keyword-only parameters after a star. It also explains how positional-only names remain available in **kwargs. A library accepting arbitrary keyword data can avoid consuming a key merely because it matches an internal parameter name.

I find this distinction valuable for library design. Python's usefulness depends heavily on well-defined protocols between libraries and callers. A signature that states its accepted call forms reduces the amount of convention that documentation alone must carry.

Neither syntax addition removes the need to choose a clear interface. A positional-only argument can make a call less descriptive if a keyword would help the reader. An assignment expression can obscure a condition if the retained value is hard to distinguish from its test. Their value depends on expressing an actual requirement in the code.

Python 3.8 now supplies assignment expressions as part of a stable release. The most direct uses retain a match result or a newly read block at the point where the program decides what to do with it. The scope and evaluation rules let that retained value participate in ordinary Python code, with the consequences visible in the containing function and its control flow.