The Walrus Operator Is Actually Useful
When Python 3.8 added :=, everyone made fun of it. The name 'walrus operator' didn't help. It looked like a solution in search of a problem.
But after using it for a while? It's grown on me. There are specific patterns where it makes code genuinely cleaner.
I resisted it at first. My initial reaction was "Python doesn't need this." But once you start noticing the patterns where it fits, you can't unsee them. Those annoying duplicate assignments, those temporary variables that exist just to be checked once—the walrus operator eliminates them elegantly.
The basic idea
The walrus operator := assigns a value AND returns it in a single expression. It's called an 'assignment expression'.
# Regular assignment - statement, can't use in expressions
x = 10
# Assignment expression - assigns AND returns the value
(x := 10) # x is now 10, and this expression evaluates to 10
The parentheses are required in most contexts to avoid ambiguity. This is intentional—Guido wanted to make sure you're being explicit about using an assignment expression.
The key difference from regular assignment (=) is that := is an expression, not a statement. Expressions have values and can be used anywhere Python expects a value. Statements don't have values and can only appear on their own lines (with some exceptions).
Where it shines: while loops
This is the classic use case. Reading from a file until EOF:
# Before - duplicated readline call
line = file.readline()
while line:
process(line)
line = file.readline()
# After - clean and DRY
while (line := file.readline()):
process(line)
No more duplicating the readline call. The assignment happens right in the while condition.
This pattern is everywhere once you start looking for it. Here's a more complex example from a log processor I wrote:
# Processing logs with multiple exit conditions
import gzip
def process_log_file(filename):
with gzip.open(filename, 'rt') as f:
line_count = 0
error_count = 0
# Read until EOF or too many errors
while (line := f.readline()) and error_count < 100:
line_count += 1
if 'ERROR' in line:
error_count += 1
print(f'Line {line_count}: {line.strip()}')
# Every 10000 lines, print progress
if line_count % 10000 == 0:
print(f'Processed {line_count} lines, {error_count} errors found')
return line_count, error_count
Same pattern works for any 'read until done' loop:
# Reading from a socket
while (data := socket.recv(1024)):
handle(data)
# Processing a queue
while (item := queue.get_nowait()):
process(item)
# User input
while (command := input('> ')) != 'quit':
execute(command)
# Pagination API calls
page = 1
all_results = []
while (response := fetch_api_page(page)).get('has_more'):
all_results.extend(response['data'])
page += 1
The pattern is always the same: fetch, check, and use in a single expression.
Where it shines: if statements
When you need to check something and also use it:
# Before - separate assignment and check
match = pattern.search(text)
if match:
print(match.group())
# After - combined
if (match := pattern.search(text)):
print(match.group())
The match variable is still available inside the if block.
More real-world examples:
# Check for a user and use them
if (user := get_user(user_id)):
send_email(user.email)
else:
log.warning('User not found', user_id=user_id)
# Check environment variable
if (api_key := os.environ.get('API_KEY')):
client = APIClient(api_key)
else:
raise ValueError('API_KEY not set')
# Fetch and validate
if (data := fetch_data()) and validate(data):
process(data)
Where it shines: list comprehensions
Avoid computing the same thing twice:
# Before - calls slow_function twice per item
results = [slow_function(x) for x in items if slow_function(x) > threshold]
# After - calls slow_function once per item
results = [y for x in items if (y := slow_function(x)) > threshold]
This can be a significant performance improvement if slow_function is expensive.
Another example with filtering and transforming:
# Get valid results from parsing
valid_results = [
parsed
for line in lines
if (parsed := try_parse(line)) is not None
]
Here's a real-world example from data processing:
# Processing API responses - only keep successful ones
import requests
urls = ['https://api.example.com/users/1', 'https://api.example.com/users/2']
# Before - ugly nested comprehension or filter/map
data = [r.json() for r in [requests.get(url) for url in urls] if r.status_code == 200]
# After - much clearer
data = [
r.json()
for url in urls
if (r := requests.get(url)).status_code == 200
]
The walrus operator captures the response object so we can both check it and use it.
Where it shines: any() and all()
Find the first match and save it:
# Find first even number
if any((n := x) for x in numbers if x % 2 == 0):
print(f'First even: {n}')
# Find first match
if any((result := check(item)) for item in items):
handle(result)
This is admittedly getting clever, so use with caution.
A practical example from testing:
# Check if any test failed and report the first failure
test_functions = [test_login, test_checkout, test_payment]
if any((error := test()) for test in test_functions):
print(f'Test failed with error: {error}')
sys.exit(1)
else:
print('All tests passed!')
The variable assigned in the walrus operator is available in the surrounding scope, not just inside the comprehension. This is a key feature that makes this pattern useful.
Working with switch statements (match/case)
Python 3.10 added match/case statements, and the walrus operator works great with them:
# Before - need to assign first
def handle_response(response):
status_code = response.status_code
match status_code:
case 200:
return response.json()
case 404:
return None
case _:
raise HTTPError(status_code)
# After - assign in the match statement
def handle_response(response):
match (status := response.status_code):
case 200:
return response.json()
case 404:
print(f'Not found (status {status})')
return None
case _:
raise HTTPError(f'Unexpected status: {status}')
You can also use it in the case patterns themselves for more complex matching:
def process_event(event):
match event:
case {'type': 'user_action', 'data': data} if (user_id := data.get('user_id')):
log_user_action(user_id, data)
case {'type': 'system_event', 'level': level} if (level := level.upper()) == 'ERROR':
alert_on_call(event)
case _:
log_unknown_event(event)
Common patterns
Caching expensive calls:
def get_config():
if (cfg := getattr(get_config, '_cache', None)) is None:
cfg = get_config._cache = load_config()
return cfg
Chained conditions:
if (user := get_user(id)) and (profile := user.get_profile()):
display(profile)
Debugging without changing flow:
# Logs the value while still using it in the condition
if (result := compute()) > threshold:
print(f'DEBUG: {result=}')
process(result)
Processing batches:
# Process data in chunks from a generator
def process_batches(data_stream, batch_size=100):
batch = []
for item in data_stream:
batch.append(item)
if len(batch) >= batch_size:
process_batch(batch)
batch = []
# Don't forget the last partial batch
if batch:
process_batch(batch)
# With walrus operator - cleaner batch handling
def process_batches(data_stream, batch_size=100):
iterator = iter(data_stream)
while (batch := list(itertools.islice(iterator, batch_size))):
process_batch(batch)
Dictionary lookups with defaults:
# Before - need to check then access
cache = {}
if key in cache:
value = cache[key]
else:
value = compute_expensive_value(key)
cache[key] = value
# After - more concise
cache = {}
if (value := cache.get(key)) is None:
value = cache[key] = compute_expensive_value(key)
Scoping gotchas
The walrus operator has some scoping behavior you need to understand:
# The variable leaks to the outer scope
[y := x**2 for x in range(5)]
print(y) # Prints 16 (the last value assigned)
# This is different from regular for loop variables in comprehensions
[x**2 for x in range(5)]
# print(x) # NameError in Python 3.x
This "leaking" is actually intentional and what makes patterns like any() useful. But it can surprise you if you're not expecting it.
Another gotcha - you can't use walrus operator at the module level or in class definitions without parentheses:
# SyntaxError
x := 5
# OK
(x := 5)
# But why would you do that anyway?
x = 5 # Just use normal assignment
Performance considerations
The walrus operator doesn't make your code faster—it just makes it more concise. In fact, there's essentially zero performance difference:
# These are equivalent in performance
# Version 1
x = expensive_call()
if x:
process(x)
# Version 2
if (x := expensive_call()):
process(x)
The real performance win comes from avoiding duplicate calls:
# Calls expensive_function twice per item - BAD
results = [expensive_function(x) for x in items if expensive_function(x) > 0]
# Calls expensive_function once per item - GOOD
results = [val for x in items if (val := expensive_function(x)) > 0]
This isn't specific to the walrus operator—it's just that the walrus operator makes it easier to write the efficient version.
Where NOT to use it
Don't get too clever. If the line is hard to read, split it up. Readability beats cleverness.
Too complex - just split it:
# Don't do this
if (x := foo()) and (y := bar(x)) and (z := baz(x, y)):
use(x, y, z)
# Do this instead
x = foo()
if x:
y = bar(x)
if y:
z = baz(x, y)
if z:
use(x, y, z)
Simple cases don't need it:
# Overkill
if (x := 5) > 3:
print(x)
# Just use normal assignment
x = 5
if x > 3:
print(x)
When the variable name is misleading:
# Confusing - what is 'm'?
if (m := re.search(pattern, text)):
print(m.group())
# Better - clear variable name
if (match := re.search(pattern, text)):
print(match.group())
# Even better in many cases - just split it up
match = re.search(pattern, text)
if match:
print(match.group())
In lambda functions (you can't):
# SyntaxError - not allowed in lambda
f = lambda x: (y := x + 1)
# Use a regular function instead
def f(x):
y = x + 1
return y
Common mistakes and how to fix them
Mistake 1: Forgetting parentheses
# SyntaxError - missing parentheses
if x := get_value() > 10:
print(x)
# Correct
if (x := get_value()) > 10:
print(x)
The parentheses aren't always required (like in while loops), but it's good practice to always use them for clarity.
Mistake 2: Using walrus in function arguments
# This works but is confusing
result = function(x := compute_value())
# Better - assign first
x = compute_value()
result = function(x)
The walrus operator in function arguments makes it unclear when the assignment happens and what scope the variable lives in.
Mistake 3: Overusing in comprehensions
# Too clever - hard to read
results = [
(x, y, z)
for i in range(10)
if (x := compute_x(i)) > 0
if (y := compute_y(x)) > 0
if (z := compute_z(x, y)) > 0
]
# Better - just use a regular loop
results = []
for i in range(10):
x = compute_x(i)
if x > 0:
y = compute_y(x)
if y > 0:
z = compute_z(x, y)
if z > 0:
results.append((x, y, z))
Mistake 4: Confusing assignment order
# This doesn't work - y isn't defined yet
if (x := y + 1) and (y := get_value()):
print(x, y)
# Correct - assign y first
if (y := get_value()) and (x := y + 1):
print(x, y)
The walrus operator evaluates left to right, just like any other expression.
Debugging tips
The walrus operator can make debugging slightly trickier because you can't easily insert a breakpoint between the assignment and the use. Here are some tips:
Tip 1: Temporary expansion for debugging
# Production code - compact
while (line := file.readline()):
process(line)
# Debugging version - expanded
line = file.readline()
while line:
# Set breakpoint here
process(line)
line = file.readline()
Tip 2: Print debugging with walrus
# You can actually use walrus for debugging
if (user := get_user(user_id)) or print(f'User {user_id} not found'):
send_email(user.email)
# Or assign and print in one go
while (line := file.readline()) and (print(f'Read: {line[:50]}...') or True):
process(line)
This is a bit hacky, but sometimes useful for quick debugging.
Tip 3: Use descriptive variable names
# Hard to debug - what is 'm'?
if (m := re.search(pattern, text)):
result = m.group(1)
# Easier to debug - clear what 'match' is
if (match := re.search(pattern, text)):
result = match.group(1)
When debugging, clear variable names make it much easier to understand what's happening in your debugger.
Real-world case study: refactoring with walrus
Let me show you a before-and-after from actual code I refactored. This is from a web scraping script:
# Before - lots of duplication and temporary variables
def extract_product_info(html):
title_match = re.search(r'<h1>(.+?)</h1>', html)
if title_match:
title = title_match.group(1)
else:
title = None
price_match = re.search(r'\$(\d+\.\d{2})', html)
if price_match:
price = float(price_match.group(1))
else:
price = None
rating_match = re.search(r'(\d\.\d) stars', html)
if rating_match:
rating = float(rating_match.group(1))
else:
rating = None
if title and price:
return {'title': title, 'price': price, 'rating': rating}
return None
# After - cleaner with walrus operator
def extract_product_info(html):
if (title_match := re.search(r'<h1>(.+?)</h1>', html)) and \
(price_match := re.search(r'\$(\d+\.\d{2})', html)):
return {
'title': title_match.group(1),
'price': float(price_match.group(1)),
'rating': float(rating_match.group(1))
if (rating_match := re.search(r'(\d\.\d) stars', html))
else None
}
return None
Is the second version "better"? Depends on your team and style guide. It's more compact, but some might find it harder to debug. The point is: you have options now.
Comparison with other languages
If you've used other languages, the walrus operator might feel familiar:
// C - assignment in conditions is common
if ((file = fopen("data.txt", "r")) != NULL) {
process(file);
}
// JavaScript - assignment in conditions
let match;
if (match = /pattern/.exec(text)) {
console.log(match[0]);
}
Python resisted this for years because = vs == confusion led to bugs in C and JavaScript. The walrus operator := solves this by using a different symbol that can't be confused with comparison.
Team adoption tips
If you want to start using the walrus operator on a team:
-
Start with while loops - these are the clearest wins and least controversial.
-
Use it in code reviews - suggest it when reviewing duplicate assignments, but don't be dogmatic.
-
Update your style guide - agree on when it's appropriate. Our team's rule: "Use it if it eliminates duplication AND improves readability."
-
Avoid in public APIs - keep your public-facing code simple. Internal helper functions are fine.
-
Consider your Python version - only works in 3.8+. If you're still supporting older versions, you'll need to wait.
The bottom line
The walrus operator is useful when:
- You need to assign and use a value in the same expression
- You're reading in a loop until a condition
- You want to avoid computing the same thing twice
It's not a revolution, but it eliminates some annoying patterns. Use it where it makes code cleaner, skip it where it makes code confusing.
My rule of thumb: if I have to pause and think about what a walrus operator is doing, it probably shouldn't be there. The best uses are the ones where it just feels obvious—like eliminating a duplicated readline() call.
Give it a try. You might find yourself reaching for it more than you expected.