The walrus operator :=
can be really helpful, but if you use it in convoluted
ways it will make your code worse instead of better.
Use :=
to flatten a sequence of nested if
s or to reuse partial computations.
(If you are new here and have no idea what a Pydon't is, you may want to read the Pydon't Manifesto.)
The walrus operator is written as :=
(a colon and an equality sign) and was first
introduced in Python 3.8.
The walrus operator is used in assignment expressions, which means assignments can
now be used as a part of an expression, whereas before Python 3.8 the assignments were
only possible as statements.
An assignment statement assigns a value to a variable name, and that is it. With an assignment expression, that value can then be immediately reused. Here is an example of the difference:
>>> a = 3
>>> print(a)
3
>>> print(b = 3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'b' is an invalid keyword argument for print()
>>> print(b := 3)
3
>>> b
3
As shown in PEP 572, a good usage of assignment expressions can help write better code: code that is clearer and/or runs faster.
Assignment expressions should be avoided when they make the code too convoluted, even if it saves you a couple of lines of code. You don't want to disrespect the Zen of Python, and the Zen of Python recommends writing readable code.
The snippet of code below features what is, in my opinion, a fairly unreadable usage of an assignment expression:
import sys
if (i := input())[0] == "q" or i == "exit":
sys.exit()
I think a better alternative would have been
import sys
i = input()
if i[0] == "q" or i == "exit":
sys.exit()
The second alternative (without :=
) is much easier to read than the first one, even
though using :=
saved one line of code.
However, good uses of assignment expressions can
Here are a couple of examples of good usages of assignment expressions.
Consider the following while
loop:
inp = input()
while inp:
eval(inp)
inp = input()
This code can be used to create a very basic Python repl inside your Python program,
and the REPL stops once you give it an empty input,
but notice that it features some repetition.
First, you have to initialise inp
, because you want to use it in your while
condition, but then you also have to update inp
inside your while
loop.
With an assignment expression, the above can be rewritten as:
while inp := input(" >> "):
eval(inp)
This not only makes the code shorter, but it makes it more expressive, by making it
blatantly clear that it is the user input provided by input()
that is controlling
the while
loop.
Say you want to count the number of trailing zeroes in an integer. An easy way to do so would be to convert the integer to a string, find its length, and then subtract the length of that same string with all its trailing zeroes removed. You could write it like so:
def trailing_zeroes(n):
s = str(n)
return len(s) - len(s.rstrip("0"))
However, for a function so simple and so short, it kind of looks sad to have
such a short s = str(n)
line represent half of the body of the trailing_zeroes
function.
With an assignment expression, you can rewrite the above as
def trailing_zeroes(n):
return len(s := str(n)) - len(s.rstrip("0"))
The function above can be read as βreturn the length of the string s
you get from
n
, minus the length of s
without trailing zeroesβ, so the assignment expression
doesn't hurt the readability of the function and, in my opinion, improves it.
Feel free to disagree, of course, as this is not an objective matter.
Suppose you are writing a list comprehension with an if
filter,
but the filter test in the
comprehension uses a value that you also want to use in the list itself.
For example, you have a list of integers, and want to keep the factorials of the
numbers for which the factorial has more than \(50\) trailing zeroes.
You could do this like so:
from math import factorial as fact
l = [3, 17, 89, 15, 58, 193]
facts = [fact(num) for num in l if trailing_zeroes(fact(num)) > 50]
The problem is that the code above computes the factorial for each number twice, and if the numbers get big, this can become really slow. Using assignment expressions, this could become
from math import factorial as fact
l = [3, 17, 89, 15, 58, 193]
facts = [f for num in l if trailing_zeroes(f := fact(num)) > 50]
The use of :=
allows to reuse the expensive computation of the factorial of num
.
Two other similar alternatives, without assignment expressions, would be
from math import factorial as fact
l = [3, 17, 89, 15, 58, 193]
# Alternative 1
facts = [fact(num) for num in l]
facts = [num for num in facts if trailing_zeroes(num) > 50]
# Alternative 2
facts = [num for num in map(fact, l) if trailing_zeroes(num) > 50]
Notice that the second one can be more memory efficient if your list l
is large:
the first alternative first computes the whole list of factorials,
whereas the second alternative only computes the factorials as they are needed.
(I'll write more about this in a later Pydon't, subscribe so you don't miss it!)
Imagine you reach a point in your code where you need to pick an operation to do to
your data, and you have a series of things you would like to try.
But you also would like to stick to the first one that works.
As a very simple example, suppose we have a string that may contain an email or a phone
number, and you would like to extract the email and, in case you find none, you look
for the phone number.
(For the sake of simplicity, let's assume phone numbers are \(9\) digits long and let's
also consider simple .com
emails with only letters.)
You could do something like:
import re
string = input("Your contact info: >> ")
email = re.search(r"\b(\w+@\w+\.com)\b", string)
if email:
print(f"Your email is {email.group(1)}.")
else:
phone = re.search(r"\d{9}", string)
if phone:
print(f"Your phone is {phone.group(0)}.")
else:
print("No info found...")
Notice the code above is nested, but the logic is flat: we look for successive things and stop as soon as we find something. With assignment expressions this could be rewritten as:
import re
string = input("Your contact info: >> ")
if email := re.search(r"\b(\w+@\w+\.com)\b", string):
print(f"Your email is {email.group(1)}.")
elif phone := re.search(r"\d{9}", string):
print(f"Your phone is {phone.group(0)}.")
else:
print("No info found...")
Assignment expressions allow the binding of a name to a part of an expression, which can be used to great benefit in clarifying the flow of some programs or saving time on expensive computations, for example. Bad usages of assignment expressions, however, can make code very unreadable and is therefore crucial to judge whether or not an assignment expression is a good fit for a particular task.
If you liked this Pydon't be sure to leave a reaction below and share this with your friends and fellow Pythonistas.
Also, don't forget to subscribe to the newsletter so you don't miss a single Pydon't!
Online references consulted on the 26th of January of 2021.
+35 chapters. +400 pages. Hundreds of examples. Over 30,000 readers!
My book βPydon'tsβ teaches you how to write elegant, expressive, and Pythonic code, to help you become a better developer. >>> Download it here ππ.