Fundamentals 9 min read

Understanding Python’s Walrus Operator (Assignment Expressions)

The article explains Python’s walrus operator (:=), introduced in version 3.8, covering its syntax, readability considerations, basic usage, and numerous practical scenarios such as conditional checks, loops, comprehensions, exception handling, and asynchronous code with clear code examples.

Test Development Learning Exchange
Test Development Learning Exchange
Test Development Learning Exchange
Understanding Python’s Walrus Operator (Assignment Expressions)

The walrus operator, formally called Assignment Expressions and introduced in Python 3.8, uses the := symbol to perform assignment inside an expression, allowing a value to be both assigned to a variable and used immediately, which can simplify code and reduce temporary variables.

Readability note: While the operator can shorten code, excessive or inappropriate use may hurt readability; developers should balance conciseness with clarity and prefer traditional assignment when it makes the code more understandable.

Basic usage:

variable := expression

Here variable receives the result of expression and the whole expression evaluates to that result.

Typical application scenarios with examples:

1. In conditional statements:

Traditional form:

length = len(some_list)
if length > 10:
    print(f"List is too long ({length} elements, expected <= 10)")

Using the walrus operator:

if (n := len(some_list)) > 10:
    print(f"List is too long ({n} elements, expected <= 10)")

2. In loops:

while True:
    response = make_api_call()
    if response.is_success:
        break
# With walrus
while not (response := make_api_call()).is_success:
    pass

3. In list comprehensions or generator expressions:

# Traditional
long_words = []
for sentence in sentences:
    words = sentence.split()
    for word in words:
        if len(word) > 5:
            long_words.append(word)
# With walrus
long_words = [word for sentence in sentences if (words := sentence.split()) for word in words if len(word) > 5]

4. Simplifying exception handling:

try:
    config_value = get_config_from_file('key')
except FileNotFoundError:
    config_value = get_default_config('key')
# With walrus
config_value = get_config_from_file('key') if (value := get_config_from_file('key')) is not None else get_default_config('key')

5. Using enumerate() to get index and value together:

for i, item in enumerate(items):
    if i == (index := items.index(some_value)):
        print(f"Found {some_value} at index {i}")
        break
# With walrus
for i, item in enumerate(items):
    if item == some_value and (index := i) is not None:
        print(f"Found {some_value} at index {index}")
        break

6. Directly processing re.findall() results:

matches = re.findall(pattern, text)
for match in matches:
    process_match(match)
# With walrus
for match in (match := re.findall(pattern, text)):
    process_match(match)

7. Simplifying nested dictionary access:

data = {'outer': {'inner': 'value'}}
if 'outer' in data and 'inner' in data['outer']:
    value = data['outer']['inner']
# With walrus
if (value := data.get('outer', {}).get('inner')) is not None:
    print(value)

8. Using itertools.groupby() to get key and aggregate:

from itertools import groupby
data = [{'category': 'A', 'value': 1}, {'category': 'B', 'value': 2}, {'category': 'A', 'value': 3}]
for category, group in groupby(data, lambda x: x['category']):
    total = sum(item['value'] for item in group)
    print(f"Category {category}: Total {total}")
# With walrus
for category, (total := sum(item['value'] for item in group)) in groupby(data, lambda x: x['category']):
    print(f"Category {category}: Total {total}")

9. Handling multiple sequences with zip() :

list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']
for i, (num, letter) in enumerate(zip(list1, list2)):
    if num == (target_num := 2):
        print(f"At index {i}, found number {target_num} with corresponding letter {letter}")
# With walrus (same as above because assignment already occurs)

10. In asynchronous code with asyncio.gather() :

import asyncio
async def fetch_data(url):
    # ...fetching logic...
    pass
tasks = [fetch_data(url) for url in urls]
results = await asyncio.gather(*tasks)
for result in results:
    process_result(result)
# With walrus (example shows refactoring into a helper function)

11. Reducing function calls in functools.reduce() :

from functools import reduce
data = [1, 2, 3, 4, 5]
sum = reduce(lambda acc, x: acc + x, data, 0)
# With walrus
sum = reduce(lambda acc, x: (acc := acc + x), data, 0)

12. Processing results in map() :

numbers = [1, 2, 3, 4, 5]
squares = map(lambda x: x ** 2, numbers)
for square in squares:
    print(square)
# With walrus
squares = map(lambda x: (square := x ** 2), numbers)
for square in squares:
    print(square)

13. Filtering with filter() while capturing intermediate values:

numbers = [1, 2, 3, 4, 5]
even_numbers = filter(lambda x: x % 2 == 0, numbers)
for even in even_numbers:
    print(even)
# With walrus
even_numbers = filter(lambda x: (is_even := x % 2 == 0) and is_even, numbers)
for even in even_numbers:
    print(even)

Overall, the walrus operator can make Python code more concise in many contexts, but developers should use it judiciously to maintain readability.

code optimizationPython 3.8walrus-operatorassignment-expressions
Test Development Learning Exchange
Written by

Test Development Learning Exchange

Test Development Learning Exchange

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.