[Python-Dev] PEP 572: Assignment Expressions

Nick Coghlan ncoghlan at gmail.com
Tue Apr 17 08:17:31 EDT 2018


On 17 April 2018 at 17:46, Chris Angelico <rosuav at gmail.com> wrote:
> Syntax and semantics
> ====================
>
> In any context where arbitrary Python expressions can be used, a **named
> expression** can appear. This is of the form ``target := expr`` where
> ``expr`` is any valid Python expression, and ``target`` is any valid
> assignment target.

The "assignment expressions should be restricted to names only"
subthread from python-ideas finally crystallised for me (thanks in
part to your own comment that 'With regular assignment (whether it's
to a simple name or to a subscript/attribute), removing the "target
:=" part will leave you with the same value - the value of "x := 1" is
1.'), and I now have a concrete argument for why I think we want to
restrict the assignment targets to names only: all complex assignment
targets create inherent ambiguity around the type of the expression
result, and exactly which operations are performed as part of the
assignment.

Initially I thought the problem was specific to tuple unpacking
syntax, but attempting to explain why subscript assignment and
attribute assignments were OK made me realise that they're actually
even worse off (since they can execute arbitrary code on both setting
and retrieval, whereas tuple unpacking only iterates over iterables).

Tackling those in order...

Tuple unpacking:

    What's the result type for "a, b, c := range(3)"? Is it a range()
object? Or is it a 3-tuple? If it's a 3-tuple, is that 3-tuple "(1, 2,
3)" or "(a, b, range(3))"?
    Once you have your answer, what about "a, b, c := iter(range(3))"
or "a, b, *c := range(10)"?

Whichever answers we chose would be surprising at least some of the
time, so it seems simplest to disallow such ambiguous constructs, such
that the only possible interpretation is as "(a, b, range(3))"

Subscript assignment:

    What's the final value of "result" in "seq = list(); result =
(seq[:] := range(3))"? Is it "range(3)"? Or is it "[1, 2, 3]"?
    As for tuple unpacking, does your preferred answer change for the
case of "seq[:] := iter(range(3))"?

    More generally, if I write  "container[k] := value", does only
"type(container).__setitem__" get called, or does
"type(container).__getitem__" get called as well?

Again, this seems inherently ambiguous to me, and hence best avoided
(at least for now), such that the result is always unambiguously
"range(3)".

Attribute assignment:

    If I write  "obj.attr := value", does only "type(obj).__setattr__"
get called, or does "type(obj).__getattribute__" get called as well?

While I can't think of a simple obviously ambiguous example using
builtins or the standard library, result ambiguity exists even for the
attribute access case, since type or value coercion may occur either
when setting the attribute, or when retrieving it, so it makes a
difference as to whether a reference to the right hand side is passed
through directly as the assignment expression result, or if the
attribute is stored and then retrieved again.

If all these constructs are prohibited, then a simple design principle
serves to explain both their absence and the absence of the augmented
assignment variants: "allowing the more complex forms of assignment as
expressions makes the order of operations (as well as exactly which
operations are executed) inherently ambiguous".

That ambiguity generally doesn't exist with simple name bindings (I'm
excluding execution namespaces with exotic binding behaviour from
consideration here, as the consequences of trying to work with those
are clearly on the folks defining and using them).

> 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::
>
>     # 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]

[snip]

> Style guide recommendations
> ===========================
>
> As this adds another way to spell some of the same effects as can already be
> done, it is worth noting a few broad recommendations. These could be included
> in PEP 8 and/or other style guides.
>
> 1. If either assignment statements or assignment expressions can be
>    used, prefer statements; they are a clear declaration of intent.
>
> 2. If using assignment expressions would lead to ambiguity about
>    execution order, restructure it to use statements instead.
>
> 3. Chaining multiple assignment expressions should generally be avoided.
>    More than one assignment per expression can detract from readability.

Given the many different uses for ":" identified on python-ideas, I'm
inclined to suggest making these proposed style guidelines more
prescriptive (at least initially) by either:

1. Listing out specific approved unambiguous use cases (i.e. if
statement conditions, while loop conditions, list comprehensions,
generation expressions)
2. Making the 3rd admonition more general by advising against using
":" for more than one purpose in the same expression (i.e. don't
combine assignment expressions with slicing syntax, lambda
expressions, function headers, variable annotations, dict or set
displays, dict or set comprehensions)

Cheers,
Nick.

-- 
Nick Coghlan   |   ncoghlan at gmail.com   |   Brisbane, Australia


More information about the Python-Dev mailing list