loading...
Cover image for The Controversy Behind The Walrus Operator in Python

The Controversy Behind The Walrus Operator in Python

renegadecoder94 profile image Jeremy Grifski Originally published at therenegadecoder.com on ・9 min read

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:

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

Into this:

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:

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:

 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:

(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):

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

Into this:

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:

[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:

[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:

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

Here, we have a list that looks like this:

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

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

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:

x = y = z = 0 # Equivalent: (z := (y := (x := 0)))

x = 1, 2 # Sets x to (1, 2)(x := 1, 2) # Sets x to 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!

Support

With all that said, thanks again for showing your support and checking out my work. If you’re new here, I’d appreciate it if you hopped on my mailing list or even joined me on Patreon. If you do decide to lay down some cash, there’s a ton in it for you including having an article written about you, getting coupon codes to the store, and gaining access to premium articles.

Speaking of my store, you might find some value in my Python 3 Cheat Sheet which features two full pages of beginner-related language features like loops, conditionals, and comprehensions. If you head over there now, you can catch 10% off with the coupon code RENEGADE.

Alternatively, you’re welcome to stick around and check out some of my other Python articles:

As always, thanks for stopping by! See you back soon.

The post The Controversy Behind The Walrus Operator in Python appeared first on The Renegade Coder.

Posted on by:

renegadecoder94 profile

Jeremy Grifski

@renegadecoder94

Engineering Education PhD student interested in challenging cultural issues in the tech community.

Discussion

markdown guide
 

Great article! Thank you

As this is the behavior of C, I presume this behavior was consciously omitted in Python. Is there documentation of the original decision?

I look on this with the expectation of having a pre-processor. Thus I ask myself, why not just declare a macro for those who will use it? This is in essence how Objective-C works.

"Those who will use it." The "in favor" examples read to me (with my C-dev eyes) as already rather exotic. If a wide-spread use of Python already strays from Zen perhaps this is the interesting thing, the ideal vs living language. That walrus took some heat is a footnote, prompting interest in why now? why not then?

Another note I don't seem to find: van Rossum resigned as BDFL in 2018. I'd like to read thoughts about how this resignation impacted walrus. Some people seem to suggest walrus caused resignation, but I'm not seeing data to support this view. To what extent is walrus simply a stretching out, given Dad no longer does our driving?

Perhaps the story is idealistic youth gives way to practicality in deployment, peeling out as sheer pleasure. Who wants Peter Pan, still uses Python?

 

Honestly, I'm really happy it's coming. I was a bit apprehensive too initially but ever since then I have started noticing places where the walrus operator would really simplify my code. I don't like writing the double lines personally, I really feel this optimization is nice.

 

Do you have any cool examples? I’m actually pretty excited to see how the community uses this operator.

 

Have a cool example now. Calculating diff b/w subsequent values.

>>> data = [10, 14, 34, 49, 70, 77]
>>> prev = 0; 
>>> [-prev + (prev := x) for x in data]
[10, 4, 20, 15, 21, 7]
 

I have yet to port my bigger projects to it but if I write something cool I'll let ya know.

 

It feels like a pain to read the code since essentially your putting 2 lines of code together.

Sure in terms of optimisation and cleverness is good but my only worry is that is its maintainability of this piece of code.

Even for string literals despite it has been out for 2 years. It's only recently that I started using it in my python code as it was not within my purview previously on it's usefullness.

 

Yeah, I’m generally against increasing the complexity of languages. That’s why I like python so much to begin with. I feel like this doesn’t add enough for it to be worth it.

 

I take note of the framing "complexity of language." We have a living industry, existing and future code, folks engaging source, tasks to perform, data to wrangle. The optimal interface (language, Python) may not be apparent, not obvious. Certain abstractions (language design decisions) optimize different engagements, even as the user base (coders) is constantly evolving, both in response and orthogonal to the language, the tasks, the install base.

To me the ideal language is one that prompts me to ask the insightful questions and to gloss all else -- given the domain on which I apply this language. [the right complexity -- and only that much -- for the job at hand]

 

In the Miscellaneous part, I think the walrus operator may come in handy when we are working with map():

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

Yeah, I like any use case where this operator removes duplicate code or computations, but I haven’t seen enough examples to be fully on board yet.