This sample is from a previous version of the book. See the new third edition here.
An assignment expression—also known as the walrus operator—is a new syntax introduced in Python 3.8 to solve a long-standing problem with the language that can cause code duplication. Whereas normal assignment statements are written a = b
and pronounced “a equals b”, these assignments are written a := b
and pronounced “a walrus b” (because :=
looks like a pair of eyeballs and tusks).
Assignment expressions are useful because they enable you to assign variables in places where assignment statements are disallowed, such as in the conditional expression of an if
statement. An assignment expression’s value evaluates to whatever was assigned to the identifier on the left side of the walrus operator.
For example, say that I have a basket of fresh fruit that I’m trying to manage for a juice bar. Here, I define the contents of the basket:
fresh_fruit = {
'apple': 10,
'banana': 8,
'lemon': 5,
}
When a customer comes to the counter to order some lemonade, I need to make sure there is at least one lemon in the basket to squeeze. Here, I do this by retrieving the count of lemons and then using an if
statement to check for a non-zero value:
def make_lemonade(count):
...
def out_of_stock():
...
count = fresh_fruit.get('lemon', 0)
if count:
make_lemonade(count)
else:
out_of_stock()
The problem with this seemingly simple code is that it’s noisier than it needs to be. The count
variable is used only within the first block of the if
statement. Defining count
above the if
statement causes it to appear to be more important than it really is, as if all code that follows, including the else
block, will need to access the count
variable, when in fact that is not the case.
This pattern of fetching a value, checking to see if it’s non-zero, and then using it is extremely common in Python. Many programmers try to work around the multiple references to count
with a variety of tricks that hurt readability (see Item 5: “Write Helper Functions Instead of Complex Expressions” for an example). Luckily, assignment expressions were added to the language to streamline exactly this type of code. Here, I rewrite this example using the walrus operator:
if count := fresh_fruit.get('lemon', 0):
make_lemonade(count)
else:
out_of_stock()
Though this is only one line shorter, it’s a lot more readable because it’s now clear that count
is only relevant to the first block of the if
statement. The assignment expression is first assigning a value to the count
variable, and then evaluating that value in the context of the if
statement to determine how to proceed with flow control. This two-step behavior—assign and then evaluate—is the fundamental nature of the walrus operator.
Lemons are quite potent, so only one is needed for my lemonade recipe, which means a non-zero check is good enough. If a customer orders a cider, though, I need to make sure that I have at least four apples. Here, I do this by fetching the count
from the fruit_basket
dictionary, and then using a comparison in the if
statement conditional expression:
def make_cider(count):
...
count = fresh_fruit.get('apple', 0)
if count >= 4:
make_cider(count)
else:
out_of_stock()
This has the same problem as the lemonade example, where the assignment of count
puts distracting emphasis on that variable. Here, I improve the clarity of this code by also using the walrus operator:
if (count := fresh_fruit.get('apple', 0)) >= 4:
make_cider(count)
else:
out_of_stock()
This works as expected and makes the code one line shorter. It’s important to note how I needed to surround the assignment expression with parentheses to compare it with 4
in the if
statement. In the lemonade example, no surrounding parentheses were required because the assignment expression stood on its own as a non-zero check; it wasn’t a subexpression of a larger expression. As with other expressions, you should avoid surrounding assignment expressions with parentheses when possible.
Another common variation of this repetitive pattern occurs when I need to assign a variable in the enclosing scope depending on some condition, and then reference that variable shortly afterward in a function call. For example, say that a customer orders some banana smoothies. In order to make them, I need to have at least two bananas’ worth of slices, or else an OutOfBananas
exception will be raised. Here, I implement this logic in a typical way:
def slice_bananas(count):
...
class OutOfBananas(Exception):
pass
def make_smoothies(count):
...
pieces = 0
count = fresh_fruit.get('banana', 0)
if count >= 2:
pieces = slice_bananas(count)
try:
smoothies = make_smoothies(pieces)
except OutOfBananas:
out_of_stock()
The other common way to do this is to put the pieces = 0
assignment in the else
block:
count = fresh_fruit.get('banana', 0)
if count >= 2:
pieces = slice_bananas(count)
else:
pieces = 0
try:
smoothies = make_smoothies(pieces)
except OutOfBananas:
out_of_stock()
This second approach can feel odd because it means that the pieces
variable has two different locations—in each block of the if
statement—where it can be initially defined. This split definition technically works because of Python’s scoping rules (see Item 21: “Know How Closures Interact with Variable Scope”), but it isn’t easy to read or discover, which is why many people prefer the construct above, where the pieces = 0
assignment is first.
The walrus operator can again be used to shorten this example by one line of code. This small change removes any emphasis on the count
variable. Now, it’s clearer that pieces
will be important beyond the if
statement:
pieces = 0
if (count := fresh_fruit.get('banana', 0)) >= 2:
pieces = slice_bananas(count)
try:
smoothies = make_smoothies(pieces)
except OutOfBananas:
out_of_stock()
Using the walrus operator also improves the readability of splitting the definition of pieces
across both parts of the if
statement. It’s easier to trace the pieces
variable when the count
definition no longer precedes the if
statement:
if (count := fresh_fruit.get('banana', 0)) >= 2:
pieces = slice_bananas(count)
else:
pieces = 0
try:
smoothies = make_smoothies(pieces)
except OutOfBananas:
out_of_stock()
One frustration that programmers who are new to Python often have is the lack of a flexible switch/case statement. The general style for approximating this type of functionality is to have a deep nesting of multiple if
, elif
, and else
statements.
For example, imagine that I want to implement a system of precedence so that each customer automatically gets the best juice available and doesn’t have to order. Here, I define logic to make it so banana smoothies are served first, followed by apple cider, and then finally lemonade:
count = fresh_fruit.get('banana', 0)
if count >= 2:
pieces = slice_bananas(count)
to_enjoy = make_smoothies(pieces)
else:
count = fresh_fruit.get('apple', 0)
if count >= 4:
to_enjoy = make_cider(count)
else:
count = fresh_fruit.get('lemon', 0)
if count:
to_enjoy = make_lemonade(count)
else:
to_enjoy = 'Nothing'
Ugly constructs like this are surprisingly common in Python code. Luckily, the walrus operator provides an elegant solution that can feel nearly as versatile as dedicated syntax for switch/case statements:
if (count := fresh_fruit.get('banana', 0)) >= 2:
pieces = slice_bananas(count)
to_enjoy = make_smoothies(pieces)
elif (count := fresh_fruit.get('apple', 0)) >= 4:
to_enjoy = make_cider(count)
elif count := fresh_fruit.get('lemon', 0):
to_enjoy = make_lemonade(count)
else:
to_enjoy = 'Nothing'
The version that uses assignment expressions is only five lines shorter than the original, but the improvement in readability is vast due to the reduction in nesting and indentation. If you ever see such ugly constructs emerge in your code, I suggest that you move them over to using the walrus operator if possible.
Another common frustration of new Python programmers is the lack of a do/while loop construct. For example, say that I want to bottle juice as new fruit is delivered until there’s no fruit remaining. Here, I implement this logic with a while
loop:
def pick_fruit():
...
def make_juice(fruit, count):
...
bottles = []
fresh_fruit = pick_fruit()
while fresh_fruit:
for fruit, count in fresh_fruit.items():
batch = make_juice(fruit, count)
bottles.extend(batch)
fresh_fruit = pick_fruit()
This is repetitive because it requires two separate fresh_fruit = pick_fruit()
calls: one before the loop to set initial conditions, and another at the end of the loop to replenish the list
of delivered fruit.
A strategy for improving code reuse in this situation is to use the loop-and-a-half idiom. This eliminates the redundant lines, but it also undermines the while
loop’s contribution by making it a dumb infinite loop. Now, all of the flow control of the loop depends on the conditional break
statement:
bottles = []
while True: # Loop
fresh_fruit = pick_fruit()
if not fresh_fruit: # And a half
break
for fruit, count in fresh_fruit.items():
batch = make_juice(fruit, count)
bottles.extend(batch)
The walrus operator obviates the need for the loop-and-a-half idiom by allowing the fresh_fruit
variable to be reassigned and then conditionally evaluated each time through the while
loop. This solution is short and easy to read, and it should be the preferred approach in your code:
bottles = []
while fresh_fruit := pick_fruit():
for fruit, count in fresh_fruit.items():
batch = make_juice(fruit, count)
bottles.extend(batch)
There are many other situations where assignment expressions can be used to eliminate redundancy (see Item 29: “Avoid Repeated Work in Comprehensions by Using Assignment Expressions” for another). In general, when you find yourself repeating the same expression or assignment multiple times within a grouping of lines, it’s time to consider using assignment expressions in order to improve readability.
Things to Remember
- Assignment expressions use the walrus operator (
:=
) to both assign and evaluate variable names in a single expression, thus reducing repetition. - When an assignment expression is a subexpression of a larger expression, it must be surrounded with parentheses.
- Although switch/case statements and do/while loops are not available in Python, their functionality can be emulated much more clearly by using assignment expressions.