Let’s start with two lines of the simplest Python code.

1
2
3
>>> name = 'piglei'
>>> print(f'Hello {name}!')
Hello piglei!

It’s a “Hello World” program, and you’ve probably seen it a million times and know every letter of it by heart. But what you probably never realized is that the two lines of code above correspond to two important concepts in Python: statement and expression.

Specifically, name = 'piglei' is an assignment line that assigns the string 'piglei' to the name variable. print(f'Hello {name}!') is an expression that prints a message to the screen by calling the built-in function print.

1. Characteristics of expressions

When writing code, statements and expressions are the two most basic types of code units.

Although we distinguish between statements and expressions in everyday expressions, the two are not completely different - an expression is actually a special kind of statement. Compared to a normal statement, an expression is special in that it has one (or more) return values.

For example, the preceding print(...) expression will return a value of None. You can get it like this.

1
2
3
4
5
# The print function always returns None
>>> val = print(f'Hello {name}!')
Hello piglei!
>>> val is None
True

While there is no practical use for doing this, it is unique enough to reflect the uniqueness of expressions - because you can never do anything like this with ordinary statements. Whether it’s an “assignment statement”, a “loop statement”, or a “conditional branch statement”, you can never assign a value to a variable, which is syntactically impossible.

1
2
3
4
5
>>> val = (name = 'piglei')
  File "<stdin>", line 1
    val = (name = 'piglei')
                ^
SyntaxError: invalid syntax #1

Not surprisingly, a syntax error (SyntaxError) is thrown.

However, with the release of Python 3.8, the line of demarcation between expressions and statements has suddenly become blurred like never before. The above line of error code can be made legal by simply adding a colon.

1
2
3
>>> val = (name := 'piglei')
>>> val, name
('piglei', 'piglei')

This is the power of the “walrus operator” – :=.

2. Walrus operator

You may wonder where the name “walrus operator” comes from, and why a walrus has suddenly appeared in the world of python. If you tilt your head 90 degrees to the left and look closely at the := symbol, you’ll see the secret: it looks like the face of a walrus, with the colon being the snout and the equal sign being its two long tusks.

Using the := operator, you can construct something called “Assignment Expressions”. Before assignment expressions, the assignment of variables could only be done by statements. Once it is available, we can assign values within a single expression and return the assigned variable.

1
>>> val = (name := 'piglei')  #1

(name := 'piglei') is an assignment expression that does two things at once: assigns 'piglei' to the name variable, and returns the value of the name variable.

Assignment expressions can be used in almost anything you can think of, such as conditional branches, loops, and list derivations, to name a few.

Let’s look at a few typical scenarios.

2.1. For branching statements

There is a function that finds the first word starting with the letter “w” in a string, and then tries to find the one ending with “w” if it is not found. The code can be written like this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import re

LEADING_W_WORD = re.compile(r'\bw\w*?\b', re.I)
TRAILING_W_WORD = re.compile(r'\b\w*?w\b', re.I)

def find_w_word(s):
    """Find and print the first word in the string that starts with w. If not found, try again to find the one that ends with w."""
    if LEADING_W_WORD.search(s):
        word = LEADING_W_WORD.search(s).group()
        print(f'Found word starts with "w": {word}')
    elif TRAILING_W_WORD.search(s):
        word = TRAILING_W_WORD.search(s).group()
        print(f'Found word ends with "w": {word}')

The result of the call is as follows.

1
2
>>> find_w_word('Guido found several examples where a programmer repeated a subexpression')
Found word starts with "w": where

There is a small problem with the above code, each expression responsible for regular search LEADING_W_WORD.search(s) is repeated twice: once at the branch judgment, and once inside the branch.

This repetition makes the code more difficult to maintain and affects the execution performance of the program. Therefore, most of the time we eliminate duplicates by defining variables.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def find_w_word_v2(s):
    l_match = LEADING_W_WORD.search(s) #1
    if l_match:
        word = l_match.group()
        print(f'Found word starts with "w": {word}')
    else:
        t_match = TRAILING_W_WORD.search(s)
        if t_match:
            word = t_match.group()
            print(f'Found word ends with "w": {word}')

Define a variable l_match to hold the match results returned by .search().

But while this eliminates repetition, it introduces a deeper nesting hierarchy that is still unsatisfactory.

With assignment expressions, we can go one step further and do the operation and assignment of the expression directly in a branching judgment statement in one go. Thus, the code can be further simplified like this.

1
2
3
4
5
6
7
def find_w_word_v3(s):
    if l_match := LEADING_W_WORD.search(s):
        word = l_match.group()
        print(f'Found word starts with "w": {word}')
    elif t_match := TRAILING_W_WORD.search(s):
        word = t_match.group()
        print(f'Found word ends with "w": {word}')

After the changes, the code is flatter and the logic is more compact.

In addition to if conditional branches, assignment expressions can be used in while loops. For example, the following pattern of looping code is very common.

1
2
3
4
5
while True:
    chunk = fp.read(2048)
    if not chunk:
        break
    # Continue with the chunk processing...

If an assignment expression is used, it can be reduced to the following.

1
2
while chunk := fp.read(2048):
    # Continue with the chunk processing...

2.2. Eliminating repetition in derivations

In addition to the previous demonstration of using assignment expressions in branching statements, you can also use it in all kinds of derivations.

As an example, when constructing a derivation, we may sometimes need to do two things at once.

  1. precompute each member and determine whether the result satisfies the requirement
  2. if so, place the precomputed result into a new object

The following code accomplishes this function.

1
2
# Only pick func(...) members of > 100 to build a new list
new_objs = [func(p) for p in objs if func(p) > 100]

While it satisfies the requirement, there is a serious problem: func(p) is repeated twice per iteration, which can be a potential performance hazard.

In the old days, if you wanted to optimize this problem, there was little you could do other than split the expression into a normal for loop. But with assignment expressions, the code can be easily optimized as follows.

1
new_objs = [v for p in objs if (v := func(p)) > 100]

The duplicate function call disappears in place.

2.3. Capturing Intermediate Results of Derivations

In some ways, an assignment expression is an expression with a “side effect” of returning a value while at the same time assigning a value to a variable. If you use this side effect intentionally, you can do some pretty surprising things.

Let me give you an example. any() is a built-in function in Python that takes an iterable object as an argument and immediately returns True if it finds any Boolean member that is true during the iteration of the object, and False otherwise.

A common usage scenario is shown below.

1
2
3
def has_lucky_number(nums):
    """Determines if there is a number in the given list that is divisible by 7"""
    return any(n % 7 == 0 for n in nums)

An example of the call is shown below.

1
2
3
4
>>> has_lucky_number([4, 8, 9])
False
>>> has_lucky_number([4, 8, 21, 9])
True

One day, the requirement changed. Not only does the function need to know if there is a number divisible by 7, but it also needs to find that number. How should the code be changed? any(...) seems like it’s not going to work anymore, so let’s write a plain for loop.

But actually, if you use assignment expressions with the short-circuiting feature of the any function, the following lines of code can also accomplish the mission.

1
2
3
4
def get_lucky_number(nums):
    if any((ret := n) % 7 == 0 for n in nums):
        return ret
    return None

Examples are as follows.

1
2
3
>>> get_lucky_number([4, 8, 9])
>>> get_lucky_number([4, 8, 21, 9])
21

The main change in the new code compared to the previous one is the replacement of n with (ret := n) - an assignment expression with side effects. As the any function loops through the nums list, the currently iterated member n is assigned to the ret variable, and if it happens to satisfy the condition, it is returned directly as the result.

With the help of side effects of assignment expressions, we successfully capture the first member that satisfies the condition and implement the requirement in just one line of code.

2.4. Limitations of assignment expressions

From the outside, an assignment expression looks very similar to an assignment statement, with the exception of a colon :. But if you dig deeper, you will find that it is actually subject to many restrictions that a normal assignment statement is not.

For example, when it is used as a whole sentence on its own, parentheses must be added on both sides.

1
2
3
4
>>> x := 1
SyntaxError: invalid syntax
>>> (x := 1)
1

In addition, assignment expressions cannot directly manipulate object properties (or keys of dictionaries).

1
2
3
4
5
6
7
8
9
# General assignment statements
>>> s.foo = 'bar'
>>> d['foo'] = 'bar'

# Assignment expressions cannot do
>>> (s.foo := 'bar')
SyntaxError: cannot use assignment expressions with attribute
>>> (d['foo'] := 'bar')
SyntaxError: cannot use assignment expressions with subscript

These kinds of restrictions are designed by the language designers to prevent people from abusing assignment expressions. But even with these restrictions, the new syntax for assignment expressions, added in Python 3.8, opens up a huge range of possibilities and imagination for “making words” in Python.

If you want to know more details about “assignment expressions”, we recommend reading the official PEP: PEP 572 - Assignment Expressions /).

3. Other suggestions

Here are two suggestions for using “assignment expressions”.

3.1. “More compact” is not the same as “better”

As shown earlier, we can combine assignment expressions like building blocks to write more compact code. But “tighter” does not equate to “better” for code. On this point, I like a simple example by Tim Peters.

Tim Peters says he doesn’t like “rushed” code and hates the idea of writing conceptually unrelated logic on the same line of code. For example, instead of writing something like this.

1
i = j = count = nerrors = 0

He prefers to change it to this.

1
2
3
i = j = 0
count = 0
nerrors = 0

The first way is compact, but it overlooks one important thing: the variables are in 3 different categories (loop index value, number and number of errors), and they all just happen to be 0. Splitting the code into 3 lines makes it less compact, but actually conceptually clearer.

When using assignment expressions, we especially need to avoid falling into the trap of blindly pursuing “refinement” and “compactness” by paying more attention to how each line of code is logically connected, rather than focusing on literally streamlining all day long.

3.2. Less is better than more

Assignment expressions are a new feature introduced in Python 3.8, and have been released for more than three years. But in my own experience, I’ve seen very little of it in other projects, except in some Python tutorials.

People rarely use assignment expressions, I guess for two main reasons.

For one thing, Python 3.8 is still a relatively new version, and many projects have not yet completed the upgrade. Second, assignment expressions are inherently flexible, applicable to a wide range of scenarios, and difficult to use in a controlled manner, so many developers are wary of them. Plus, it doesn’t offer any unique features that ordinary statements can’t do - it’s just icing on the cake - so people are reluctant to try it.

The first category of causes above will be slowly resolved over time. Let’s look mainly at the second category.

I think that most developers do have a valid concern, in that assignment expressions make the code more compact, but also bring a higher cost of understanding and a higher barrier to entry. And to be fair, some of the code that uses assignment expressions really gives me a feeling of “is that too smart?” I’d say.

Take this previous code as an example.

1
2
if any((ret := n) % 7 == 0 for n in nums):
    return ret

If it were a private script, I might be willing to write the code as above. But in a real project with multiple participants, I’d probably prefer to replace it with a plain for loop at the moment. Often, “clunky” code is what we need more than “smart” code, and it saves a lot of communication and maintenance costs for the project participants.

In general, my recommendations on whether assignment expressions should be used in projects are

  • In the elimination of repetition scenario of branching statements, use assignment expressions
  • In deduction elimination scenarios, use assignment expressions
  • In other cases, give preference to normal assignment statements, even if it means more code and less repetition (e.g. “get the first member that satisfies the condition” scenario)

I hope the above will be helpful to you.