Why can I not assign to a named expression (LHS walrus operator)?

1k views Asked by At

Assigning to expressions (as opposed to names) is commonplace in Python. For example, this is perfectly valid syntax:

my.object["with_some"].very_long["expression"] = func(my.object["with_some"].very_long["expression"], my.object["with_some"].very_long["expression"])

however, if I try to shorten it using the walrus operator by making the LHS a named expression such as

(x:=my.object["with_some"].very_long["expression"]) = func(x, x)

Python raises a SyntaxError:

SyntaxError: cannot assign to named expression

Similarly, for x[0] in range(5) is valid syntax (just mightily confusing), whereas for (a:=x[0]) in range(5) is again a SyntaxError: cannot assign to named expression.

Why can't I assign to a named expression? Is this by design or by implementation?

PEP 572 mentions some cases where the walrus operator cannot be used, but all but one are about the syntax of unparenthesised expressions and the final one is about f-strings. Unlike the situation pointed out in this answer ((self.x := ...)), the assignment target within the walrus operator in my case is a simple name/identifier, not an expression. It's not clear from the language reference either why this is not allowed. Googling the error message today yields exactly three results at the time of writing: One issue about limitations in comprehensions, a Stack Overflow chat message expecting hundreds of Hot Network Questions (which didn't happen), and an issue in a 3rd-party Python parser; none help me.

What is the reason I cannot assign to a named expression? Is this a design rule that is documented or defined somewhere, or is it an implementation limitation? As far as I can see, it does not lead to any ambiguities and it would seem that my use case should be valid.

3

There are 3 answers

0
chepner On BEST ANSWER
my.object["with_some"].very_long["expression"] = \
  func(my.object["with_some"].very_long["expression"],
       my.object["with_some"].very_long["expression"])`

is syntactic sugar for

my.object["with_some"].very_long.__setitem__(
    "expression",
    func(my.object["with_some"].very_long["expression"], 
         my.object["with_some"].very_long["expression"]))

so it's not as symmetrical as you think. The original long expression's value, not the expression itself, is passed as the two arguments to func, and the original expression itself is only kind of the target of an assignment.

You could, however, write

x["expression"] = func(
    (x:=my.object["with_some"].very_long)["expression"],
     x["expression"])

with x being assigned the value of the one common expression to the desugared version, my.object["with_some"].very_long.

The assignment expression has to be on the right-hand side of the assignment because that is evaluated before the left-hand side. Also, it has to be the first argument that uses :=, because function arguments are guaranteed to be evaluated from left to right.


Here's the test I used to verify that the above should work, assuming I defined A appropriately.

class A:
    def __init__(self, y):
        self.b = dict(foo=y)


def func(x, y):
    return x + y


a = A(A("bar"))    
x["foo"] = func((x:=a.b["foo"].b)["foo"], x["foo"])

The new value of a.b["foo"].b["foo"] is "barbar", as expected from the definition of func.

1
MarianD On

The walrus operator assigns only as a side effect - its value is not the left-hand side variable, but the result of its right-hand side expression.

So the value of the named expression

(x:=my.object["with_some"].very_long["expression"])

is the result of the right-hand side of the walrus operator (:=), i. e. the result of the expression

my.object["with_some"].very_long["expression"]

Let's denote is as result, so your command

(x:=my.object["with_some"].very_long["expression"]) = func(x, x)

is the same as

result = func(x, x)

Now, result is a value, not a variable name. Maybe None, maybe a number, maybe a particular list or something else, but not allowed in the left-hand side of the assignment operator.

So, assigning to a named expression is (at least in most cases) meaningless, and for this reason not allowed.

0
MarianD On

Compare:

my.object["with_some"].very_long["expression"] = func(7, 7)      # OK

with

7 = func(7, 7)                                                   # error

The explanation:

  1. In your first expression

    my.object["with_some"].very_long["expression"] = \
        func(my.object["with_some"].very_long["expression"],               
              my.object["with_some"].very_long["expression"])
    

    you want to change the value of an attribute of your object (based on the value of its current attribute), i.e. to do something as

    my.object["with_some"].very_long["expression"] = func(7, 7)
    
  2. In your second expression

    (x:=my.object["with_some"].very_long["expression"]) = func(x, x)
    

    the left-side hand of the assignment operator (=) is NOT an attribute of your object, but its value. So you tried assigning something to a value, i.e. to do something as

    7 = func(7, 7)
    

From PEP 572 - Assignment Expressions, Abstract:

This is a proposal for creating a way to assign to variables within an expression...

There is no mention of using it in the LHS of assignment.