I read manuals. Not because I'm some documentation hero, but because guessing wastes time. When I hit the walrus operator in someone else's code, I did what I always do: looked it up, tried it, then decided if it was useful or stupid.
The walrus operator is :=. It assigns a value inside an expression. That's it. Whether it's useful or annoying depends entirely on context.
The walrus operator: what it is (and why it exists)
Officially, := is an assignment expression. It lets you assign a value and use it in the same expression.
Basic syntax looks like this:
(value := compute())
A few details that matter in real code:
- It does not create a new scope. If you assign to
xwith:=,xis available right after, in the surrounding scope. - Parentheses are often required. Python's grammar needs help to avoid ambiguity. You'll learn this fast because the interpreter will complain at you.
- It's an expression, not a statement. That's the whole point. You can use it inside
if,while, comprehensions, and other expression contexts.
It's called "walrus" because := looks like a walrus. I voted for "beavor operator" but nobody listens to me.
Quick mental model
The pattern is simple: compute something, store it, use it immediately. If that's what you're doing, the walrus fits. If you're bending your code to use it, you're doing it wrong.
Use case #1: while loops that read input
Before :=, you'd often write this:
while True:
user_input = input("Type something (q to quit): ")
if user_input == "q":
break
print(user_input)
This is fine. It's also a common "loop + break" pattern that exists mostly because you need to both store the input and check it.
With the walrus operator:
while (user_input := input("Type something (q to quit): ")) != "q":
print(user_input)
Less scaffolding. Fewer lines. Still readable.
A practical detail: input() returns a string, so you can normalize it without repeating work:
while (cmd := input("> ").strip().lower()) not in {"q", "quit", "exit"}:
handle_command(cmd)
This stays readable until your condition becomes a paragraph. Then split it up.
Use case #2: if statements that avoid calling a function twice
Regex is a classic example:
import re
if match := re.search(r"\b\w{2,}\b", text):
print(match.group(0))
Without :=, you either call re.search() twice (don't), or you assign first and then check:
match = re.search(r"\b\w{2,}\b", text)
if match:
print(match.group(0))
Both work. Use the walrus when the variable only matters inside the conditional.
Another example: parsing user input.
if (raw := input("Age: ").strip()).isdigit():
age = int(raw)
print("Next year:", age + 1)
else:
print("Please enter a number.")
Don't jam too much into one if. If your line reads like a legal contract, break it into steps.
Use case #3: list comprehensions without recomputing
Sometimes you want to compute something once, then use it in both the filter and the output.
numbers = [1, 2, 3, 4, 5, 6]
squared_evens = [
squared
for num in numbers
if (squared := num ** 2) and num % 2 == 0
]
This works, but it's also a little awkward because the and is doing double duty. A clearer version is to filter first, then compute once:
squared_evens = [
(sq := num * num)
for num in numbers
if num % 2 == 0
]
Now the walrus is only doing what it's good at: compute once, use once, keep the value.
A more realistic example is when the computation is expensive or messy. Say you're cleaning strings:
raw_names = [" Alice ", "", " ", "Bob", None, "Eve "]
clean = [
name
for item in raw_names
if item is not None and (name := item.strip())
]
strip() runs once per item, and name is the cleaned value you actually want in the output.
This is where people get cute with it. Cute code at 2pm becomes a nightmare at 1am.
Also, note the parentheses. Python's parser is picky in comprehensions. You'll need parentheses.
Use case #4: pipelines where you process and validate
This is nice in data code:
if (processed := process_data(raw_data)) and is_valid(processed):
store(processed)
Two things:
- This pattern assumes
process_data()returns something falsy on failure (None,"",[], etc.). That can be fine, or it can hide errors. Be intentional. - If
process_data()can raise exceptions, you still needtry/except. The walrus doesn't change that.
Here's a slightly more defensive version:
try:
if (processed := process_data(raw_data)) is not None and is_valid(processed):
store(processed)
except ValueError as e:
log.warning("Bad data: %s", e)
If you're working with pandas or numpy, you'll often avoid truthiness checks because arrays don't like being coerced to True/False. In those cases, use explicit checks:
if (df := load_frame(path)) is not None and not df.empty:
...
Same idea, fewer surprises.
Real-world example: scraping with requests + BeautifulSoup
Here's what this looks like:
import requests
from bs4 import BeautifulSoup
if (resp := requests.get(url)).status_code == 200:
soup = BeautifulSoup(resp.text, "html.parser")
if (title := soup.find("title")):
print(title.get_text(strip=True))
You need the parentheses. Python won't parse this without them.
Two changes for production code:
1) Use timeout, because hanging forever is a bad hobby.
2) Use raise_for_status() if you want failures to be loud.
3) Don't assume resp.text is what you want for all pages.
Here's a version that stays readable:
import requests
from bs4 import BeautifulSoup
try:
resp = requests.get(url, timeout=10)
resp.raise_for_status()
except requests.RequestException:
return None
soup = BeautifulSoup(resp.text, "html.parser")
if title := soup.find("title"):
return title.get_text(strip=True)
return None
I didn't use the walrus everywhere because you shouldn't. Use it when it reduces repetition, not because you can.
If you really want the "single conditional" style, keep it contained:
if (resp := requests.get(url, timeout=10)).ok:
soup = BeautifulSoup(resp.text, "html.parser")
if (h1 := soup.find("h1")):
print(h1.get_text(strip=True))
Tradeoff: requests.get() can raise exceptions. If you use it inside an if, you still need a try/except around it if you care about reliability.
Real-world example: pygame input without extra calls
Game loops run a lot. Small repeated calls can add up, and they also clutter your event logic.
This pattern avoids calling pygame.mouse.get_pressed() multiple times:
import pygame
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN and (clicked := pygame.mouse.get_pressed())[0]:
handle_left_click()
This works because clicked is a tuple you might need later, and calling get_pressed() twice is wasteful.
Tradeoff: indexing into (clicked := ...)[0] is slightly dense. If you find yourself adding more conditions, split it:
elif event.type == pygame.MOUSEBUTTONDOWN:
clicked = pygame.mouse.get_pressed()
if clicked[0]:
handle_left_click()
Saving one line doesn't make you a hero.
The two big foot-guns
1) Readability death by walrus
This is technically valid:
if (n := len(a)) > 10:
...
But this is often clearer:
n = len(a)
if n > 10:
...
If the variable matters after the if, don't hide it in the condition. Make it a normal assignment.
Also, watch out for "stacked walruses":
if (a := f()) and (b := g(a)) and (c := h(b)):
...
This works, but your coworkers will hate you.
2) Scope surprises in comprehensions
This one is sneaky:
[x := i for i in range(5)]
print(x) # x is now 4
That x leaks into the surrounding scope. If you weren't expecting it, you'll waste 20 minutes wondering why your variable is wrong.
This bites you when you reuse variable names like x or tmp, or when you assume the comprehension is self-contained. Don't assign in comprehensions unless you need to, and pick specific names.
A few extra operator gotchas
x = y := 5is valid, but confusing. It assigns 5 toy, then assignsytox. It reads like a typo.(x := y) := 5is a syntax error. You can't assign to an assignment expression.- Precedence is weird. Add parentheses when mixing
:=with other operators.
What I actually do
I use it. It's useful when it reduces repetition. It's annoying when people force it into every line. That's the whole story.
The walrus operator caused enough arguing that Guido stepped down as BDFL. A punctuation mark. That's how heated it got.
-Sethers