I recently discovered the walrus operator (:=) and have mixed feelings about it.
It feels like an odd exception to Python’s usual emphasis on readability.
What puzzled me:
Python famously rejects ++/-- operators for being “unpythonic,” yet := exists.
Most cases I’ve seen (e.g., while (line := file.readline())) could just use multiple lines.
Am I missing something?
Are there legitimate cases where := is the cleanest solution?
Or is it a niche feature that encourages bad habits?
Please try to turn this exact example into multiple lines without repeating file.readline(). Yes, it’s possible, but it doesn’t read as well.
Any kind of syntax sugar can be dismissed as “just use more lines instead”, but that is missing the point.
You can search back on the mailing lists from around the time this operator was added. You will find dozens of people making the exact same arguments you are making and many people making counter arguments.
Or it isn’t . Seriously, in some contexts it’s more readable. @MegaIng and @MRAB gave examples. Like:
while buf := some_binary_file.read(BUFSIZE):
# deal with `buf`
they’re of the very simple form while result := function(). There’s really no gain in clarity (the contrary) by splitting that across multiple lines and typing buf out an additional time.
So stick to that. At the time (about 5 years ago), Guido (the language’s designer) and I were the strongest advocates for adding the walrus, and repeatedly stressed that it was intended to be used only when it actually increased clarity. I went through many thousands of lines of code “by eyeball” at the time, and only found a few dozen cases where it would be a clear improvement. But they were indeed clear (if small) improvements.
So it’s not often! If, e.g., you need to wrap an assignment expression in parentheses, you’re probably pushing the intent.
It’s intended to be a simple feature for use in simple contexts. That’s why, e.g., the binding target is restricted to be a simple identifier (no, e.g. , subscripted or attribute targets, or magical unpacking of an iterable result into multiple targets). An assignment statement can be far more complex than an assignment expression.
Python could certainly have lived without it. But in the relatively few cases where it’s “just right”, it’s very “Pythonic” to my (& Guido’s) eyes: efficiently and clearly communicating intent with a minimum of syntactic noise and/or repetition.
Of course people will, at times, use it in unintended ways. There’s nothing to stop them from, e.g., using 117 blanks per indentation level either - Python never aimed to make it impossible to write ugly code;
I use it mainly in the preprocessing steps of my functions.
Before the walrus, I was struggling to choose among many possible ways doing that kind of preprocessing :
Though - on second thoughts, it’s not the walrus that’s the problem here, it’s the assertion. This should most likely be a simple if/raise, not an assert.
This is a poor choice of example because the alternative without the Walrus operator in this case is awkward and also for line in file is better anyway.
Maybe your experience of the Walrus operator is somewhat like mine in that it appeared and then I saw other people using it enthusiastically but usually in situations where it isn’t needed. In a quick review of one codebase (git grep -C3 :=) I find that about 50% of the time when the Walrus operator is used it could just be an assignment on the line above e.g.:
if x := func():
do_thing(x)
I don’t mind it if the name being assigned to appears right after if but I also see more complex examples where the assignment can be anywhere:
if func(a, b, x := g()):
do_thing(x)
There are some while loops but the situations where the alternative without the Walrus would be awkward are almost all if/elif cases like @MRAB’s example:
if one_thing():
#
elif x := func():
do_thing(x)
elif ...
I know, but these are meant to avoid silent bugs due to inconsistently formatted data in personal codes. It is not meant for production and I use it when I don’t want to spend time and lines on exception handling.
I’m not faulting you if you haven’t done so yet, but you should read the PEP which introduced the idea, because it goes into great detail discussing the use cases for which such an operator was desired.
I will note that things like ++ are often used to modify an index during iteration, and Python already has better abstractions for iteration that don’t involve explicit indexing.
Using assert in this way is a bad habit that should be avoided all the time. Just because you happen to not be using -O right now doesn’t mean that it’s safe to abuse assertions in this way; you never know what you’ll do in the future, and the assert will cause yourself unnecessary pain.
Well, that’s right…
What I haven’t said is that I use these assertions within functions taking numerous numpy arrays with numerous dimensions, I basically use the assertions to check whether the dimension sizes are verifying some patterns.
While a first run of the script runs, the generated dimensions are checked so the -O run will just be right. Therefore using assertions here feels ok to me.
Using walrus operator in assertions must be done only if the assigned values (the dimension sizes in my cases) are only used within other assertions.
I did not want to do an OT on here, assertions are not the subject.
I read PEP572 and something did catch my attention :
lambda line: (m := re.match(pattern, line)) and m.group(1) # Valid
By carefully engineering the usage of walrus operators and and operators, one can then generate multistep (equivalent of multiline but one-lined) lambda functions. I think it opens ways to have heavy-lifting lambda functions that were not possible before. (Not sure about the use cases but probably in-topic here)
I’d normally agree with you that assert abuse should be avoided, but if as the user asserts (pun intended) this is merely a safety check that is not required in production and can be skipped for performance reasons, then the “before the walrus” example is at least arguably a legitimate use of assert.
Of course, the “all at once way” using the walrus to combine the assertion with business logic all inside the assert is fundamentally broken, as the business logic will be skipped on -O and the code will NameError.
Seems a bit abusive, since lambdas being limited to a single line is an intentional design choice to discourage non-trivial anonymous functions, and complex, “heavy-lifting” lambdas subvert their core benefit/use case (small inline functions defined and used in a single line) relative to a more readable and reusable named function.
I agree, but I am also very much aware that what you intend TODAY might not be what happens TOMORROW. If you start with this:
def frobnicate(stuff):
assert (n := stuff.length()) >= 3, "Need at least three things"
assert n < 10, "Can't have ten things"
then you have the risk that you subsequently do this:
...
more_stuff = [0] * n
without realising that you just made use of the assertion.
This might be a good use-case for an explicit if __debug__: block. It has the same risk (since there’s no way to scope the variable to just the if block), but might be a bit clearer - in debug mode, do this, do this, do this, make these checks. Not sure what I’d do in production though, as I don’t tend to use assertions at all - I prefer to have the vast majority of checks actually continue to happen in prod. Maybe that’s just cynicism, but I have been programming for a few decades, and I’ve learned not to trust myself
Right, which is exactly why in my quote I stated that only the user’s “before the walrus” example was arguably a legitimate use of assert (since only the actual check was affected, which they stated they would like to intentionally disable in production code with -O for perf reasons):
while the “all-at-once” example using the walrus operator was fundamentally flawed as it also skips the parameter assignment that is presumably used in the business logic:
Perhaps I merely misunderstood which example you were critiquing, and we are in fact in violent agreement here