Algorithms

As discussed in PEP 572, assignment expression is a way to assign to variables within an expression using the notation NAME := expr. It is roughly equivalent to first assigning expr to the variable NAME, then referencing such variable NAME at the current scope.

Basic Concepts

To convert, walrus will need to evaluate the expression, assign to the variable, make sure such variable is available from the current scope and replace the original assignment expression with code using pre-3.8 syntax.

For example, with the samples from PEP 572:

# Handle a matched regex
if (match := pattern.search(data)) is not None:
    # Do something with match

it should be converted to

# Handle a matched regex
match = pattern.search(data)
if (match) is not None:
    # Do something with match

However, such implementation is NOT generic for assignment expressions in more complex grammars and contexts, such as comprehensions per Appendix B:

a = [TARGET := EXPR for VAR in ITERABLE]

For a more generic implementation, walrus wraps the assignment operation as a function, and utilises global and nonlocal keywords to inject such assigned variable back into the original scope.

For instance, it should convert the first example to

# make sure to define ``match`` in this scope
if False:
    match = NotImplemented

def wraps(expr):
    """Wrapper function."""
    global match  # assume we're at module level
    match = expr
    return match

# Handle a matched regex
if (wraps(pattern.search(data))) is not None:
    # Do something with match

The original assignment expression is replaced with a wrapper function call, which takes the original expression part as a parameter. And in the wrapper function, it assigns the value of the expression to the original variable, and then injects such variable into outer scope (namespace) with global and/or nonlocal keyword depending on current context, and finally returns the assigned variable so that the wrapper function call works exactly as if we were using an assignment expression.

See also

Scope Keyword Selection

Python provides global and nonlocal keywords for interacting with variables not in the current namespace. Following the Python grammar definitions, walrus selects the scope keyword in the mechanism described below:

  1. If current context is at module level, i.e. neither inside a function nor a class definition, then global should be used.

  2. If current context is at function level and the variable is not declared in any global statements, then nonlocal should be used; otherwise global should be used.

  3. If current context is at class level and not in its method definition, i.e. in the class body, it shall be treated as a special case.

For assignment expression in lambda functions, it shall be treated as another special case.

Formatted String Literals

Since Python 3.6, formatted string literals (f-string) were introduced in PEP 498. And since Python 3.8, f-string debugging syntax were added to the grammar. However, when walrus performs the conversion on assignment expressions inside f-string, it may break the lexical grammar and/or the original context.

Therefore, we utilise f2format to first expand such f-string into str.format() calls, then rely on walrus to perform the conversion and processing. Basically, there are two cases as below:

  1. When an assignment expression is in a debug f-string. (To prevent the converted code from changing the original expression for self-documenting and debugging.)

  2. When an assignment expression is in an f-string inside a class scope. (To prevent the converted code from breaking the quotes of the original string.)

Lambda Functions

lambda functions can always be transformed into a regular function. This is the foundation of converting assignment expressions in lambda functions.

For a sample lambda function as follows:

>>> foo = lambda: [x := i ** 2 for i in range(10)]
>>> foo()
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

walrus will transform the original lambda function into a regular function first:

def foo():
    return [x := i ** 2 for i in range(10)]

And now, walrus can simply apply the generic conversion strategies to replace the assignment expression with a wrapper function:

def foo():
    if False:
        x = NotImplemented

    def wraps(expr):
        """Wrapper function."""
        nonlocal x  # assume that ``x`` was not declared as ``global``
        x = expr
        return x

    return [wraps(i ** 2) for i in range(10)]

Class Definitions

As the class context is slightly different from regular module and/or function contexts, the generic conversion strategies are NOT applicable to such scenarios.

Note

For method context in the class body, the generic conversion strategies are still applicable. In this section, we are generally discussing conversion related to class variables.

Given a class definition as following:

class A:
    bar = (foo := x ** 2)

walrus will rewrite all class variables in the current context:

class A:
    bar = ((__import__('builtins').locals().__setitem__('foo', x ** 2), foo)[1])

The major reason of doing so is that locals() dictionary can (and may only) be edited directly in the class declaration. Therefore, we can use this one-liner to rewrite the original assignment expression.

However, if a variable is declared in global and/or nonlocal statements, it is NOT supposed to be assigned to the class context, rather it should go to the outer scope (namespace), which will then be applicable to the regular conversion templates as discussed above.

In some corner cases, the first argument (variable name) to the __setitem__ call must be mangled and normalised instead of used directly. See bpc_utils.BaseContext.mangle() for more information.