Python

The Controversy Behind The Walrus Operator in Python

If you haven’t heard, Python 3.8 features a rather controversial new operator called the walrus operator. In this article, I’ll share some of my first impressions as well as the views from all sides. Feel free to share some of your thoughts as well in the comments.

Understanding the Walrus Operator

Recently, I was browsing dev.to, and I found a really cool article by Jason McDonald which covered a new feature in Python 3.8, the walrus operator. If you haven’t seen the operator, it looks like this: :=.

In this article, Jason states that the new operator “allows you to store and test a value in the same line.” In other words, we can compress this:

1
2
3
4
nums = [87, 71, 58]
max_range = max(nums) - min(nums)
if max_range > 30:
  # do something

Into this:

1
2
3
nums = [87, 71, 58]
if (max_range := max(nums) - min(nums)) > 30:
  # do something

In this example, we saved a line because we moved the assignment into the condition using the walrus operator. Specifically, the walrus operator performs assignment while also returning the stored value.

In this case, max_range will store 29, so we can use it later. For example, we might have a few additional conditions which leverage max_range:

1
2
3
4
5
nums = [87, 71, 58]
if (max_range := max(nums) - min(nums)) > 30:
  # do something
elif max_range < 20:
  # do something else

Of course, if you’re like me, you don’t really see the advantage. That’s why I decided to do some research.

First Impressions

When I first saw this syntax, I immediately thought “wow, this doesn’t seem like a syntax that meshes well with the Zen of Python.” In fact, after revisiting the Zen of Python, I think there are several bullet points this new syntax misses.

Beautiful Is Better Than Ugly

While beauty is in the eye of the beholder, you have to admit that an assignment statement in the middle of an expression is kind of ugly. In the example above, I had to add an extra set of parentheses to make the left expression more explicit. Unfortunately, extra parentheses reduce the beauty quite a bit.

Sparse Is Better than Dense

If the intent of the walrus operator is to compress two lines into one, then that directly contradicts “sparse is better than dense.” In the example I shared above, the first condition is fairly dense; there’s a lot to unpack. Wouldn’t it always make more sense to place the assignment on a separate line?

If you’re looking for a good example of a feature that compresses code, take a look at the list comprehension. Not only does it reduce nesting, but it also makes the process of generating a list much simpler. To be honest, I don’t get that vibe with the walrus operator. Assignment is already a pretty easy thing to do.

In the Face of Ambiguity, Refuse the Temptation to Guess.

In the example above, I introduced parentheses to make the condition more explicit. Had I left out the parentheses, it becomes a little more difficult to parse:

1
if range := max(nums) - min(nums) > 30:

In this case, we have several operators on a single line, so it’s unclear which operators take precedence. As it turns out, the arithmetic comes first. After that, the resulting integer is compared to 30. Finally, the result of that comparison (False) is stored in range and returned. Would you have guessed that when looking at this line?

To make matters worse, the walrus operator makes assignment ambiguous. In short, the walrus operator looks like a statement, but it behaves like an expression with side effects. If you’re unsure why that might be an issue, check out my article on the difference between statements and expressions.

There Should Be One—and Preferably Only One—Obvious Way to Do It.

One of the interesting things about this operator is that it now introduces a completely new way to perform assignment. In other words, it directly violates the “there should be only way way to do it” rule.

That said, I’m a little bit on the fence with this one because the new operator is more explicit. In other words, it differentiates the intent behind := and =. In addition, it reduces potential bugs related to confusing = and == in conditions.

Likewise, as far as I can tell, you can’t just use the walrus operator in all the same places you would use assignment. In fact, they’re completely different operators. Unfortunately, nothing is really stopping you from doing something like this:

1
(x := 5)

I don’t know why you would ever do this, but it’s now very legal code. Luckily, PEP 572 prohibits it. Of course, that doesn’t stop code like this from appearing in the wild. In fact, the documentation lists a handful of ways the new syntax can be abused. That’s not a good sign!

If the Implementation Is Hard to Explain, It’s a Bad Idea

At this point, I sort of drew the line with this new feature. As I dug around to read others’ opinions on the subject, I found the following gold nugget:

This feels very Perl-y in the example given, in that it requires that you know what yet another operator means to read code that uses it. Since Python is supposed to be “executable pseudocode” (roughly), this kind of new operator might increase the amount of learning that a beginner has to do to read others’ code. I hope that this decision does not pave the way for more like it, because it would make Python code much less readable to someone who hasn’t studied the new operators yet.snazz, 2019

That’s when I realized why I love Python so much. It’s just so damn easy to read. At this point, I really feel like the addition of this operator was a mistake.

Counterpoint

As with anything, I hate to form an opinion without really digging into the topic, so I decided to hear from the folks who were excited about this feature. To my surprise, I found a lot of cool examples.

Loop Variable Updates Are Easy

By far, the strongest case for the new walrus operator is in while loops. Specifically, I liked the example by Dustin Ingram which leveraged the operator to remove duplicate lines of code. For example, we can convert this (source):

1
2
3
4
chunk = file.read(8192)
while chunk:
  process(chunk)
  chunk = file.read(8192)

Into this:

1
2
while chunk := file.read(8192):
  process(chunk)

By introducing the walrus operator, we remove a duplicate line of code. Now, every time the loop iterates, we automatically update chunk without having to initialize it or update it explicitly.

Seeing this example is enough for me to see the value in the walrus operator. In fact, I’m so impressed by this example that it made me wonder where else this could be used to improve existing code.

That said, I did dig around, and some folks still felt like this was a bad example. After all, shouldn’t file reading support an iterable? That way, we could use a for loop, and this wouldn’t be an issue at all. In other words, isn’t the walrus operator just covering up for bad library design? Perhaps.

List Comprehensions Get a New Tool

As an avid list comprehension enthusiast, I’ve found that the walrus operator can actually improve efficiency by allowing us to reuse calculations. For example, we might have a comprehension which looks like this:

1
[determinant(m) for m in matrices if determinant(m) > 0]

In this example, we build up a list of determinants from a list of matrices. Of course, we only want to include matrices whose determinants are greater than zero.

Unfortunately, the determinant calculation might be expensive. In addition, if we have a lot of matrices, calculating the determinant twice per matrix could be costly. Luckily, the walrus operator is here to help:

1
[d for m in matrices if (d := determinant(m)) > 0]

Now, we only calculate the determinant once for each matrix. How slick is that?

Miscellaneous

Beyond the two examples above, I’ve seen a few other examples including pattern matching, but I don’t really have an appreciation for it. Honestly, the other examples just seem kind of niche.

For instance, PEP 572 states that the walrus operator helps with saving expensive computations. Of course, the example they provide is with constructing a list:

1
[y := f(x), y**2, y**3]

Here, we have a list that looks like this:

1
[y, y**2, y**3]

In other words, what’s stopping us from declaring y on a separate line?

1
2
y = f(x)
[y, y**2, y**3]

In the list comprehension example above, I get it, but here I don’t. Perhaps there’s a more detailed example which explains why we’d need to embed an assignment statement in list creation. If you have one, feel free to share it in the comments.

Assessment

Now that I’ve had a chance to look at the new walrus operator more or less objectively, I have to say that I think my first impressions still stand, but I’m willing to be persuaded otherwise.

After seeing a few solid examples, I was still really skeptical, so I decided to take a look at the rationale behind the operator in PEP 572. If you get a chance, take a look at that document because it’s enormous. Clearly, this decision was well thought out. My only fear is that the authors were persuaded to include the feature by shear sunk cost fallacy, but who knows.

If you read through PEP 572, you’ll see 79 code blocks across the entire page. To me, that’s just a ridiculous amount of examples. To make matters worse, a large portion of the examples show edge cases where the operator won’t work or wouldn’t be ideal rather than where it would provide an edge. For instance, take a look at some of these examples:

1
x = y = z = 0  # Equivalent: (z := (y := (x := 0)))
1
2
x = 1, 2  # Sets x to (1, 2)
(x := 1, 2)  # Sets x to 1
1
total += tax  # Equivalent: (total := total + tax)

That said, the authors did go as far as to provide some examples from their reworked standard library. Of course, these examples are much larger, so I won’t share them here. However, you’re welcome to take a peek.

Personally, I think the examples linked above illustrate the advantage of the walrus operator much better than some of the cases I shared in the counterpoint section. Specifically, any time the walrus operator removes duplicate or nested code, I’m happy with it. Otherwise, it seems to have very few obvious use cases.

My worry is that adding a new operator adds unnecessary complexity to the language, and I’m not convinced that the pros outweigh the cons. At any rate, I trust the decision of the team that put it together, and I’m excited to see how the community uses it!

Published on Web Code Geeks with permission by Jeremy Grifski, partner at our WCG program. See the original article here: The Controversy Behind The Walrus Operator in Python

Opinions expressed by Web Code Geeks contributors are their own.

Jeremy Grifski

Jeremy is the founder of The Renegade Coder, a software curriculum website launched in 2017. In addition, he is a PhD student with an interest in education and data visualization.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button