Python's Walrus Operator Cover Image

Python's Walrus Operator

Beautiful is better than ugly.

The Zen of Python

Python introduced a brand new way to assign values to variables in version 3.8.0. The new syntax is :=, and it’s called a “walrus operator” because it looks like a pair of eyes and a set of tusks. The walrus operator assigns values as part of a larger expression, and it can significantly increase legibility in many areas.

Named Expressions

You can create named expressions with the walrus operator. Named expressions have the format NAME := expression, such as x := 34 or numbers := list(range(10)). Python code can use the expression to evaluate a larger expression (such as an if statement), and the variable NAME is assigned the value of the expression.

If you’ve written Swift code before, Python’s walrus operator is similar to Swift’s Optional Chaining. With optional chaining, you assign a value to a variable inside the conditional of an if statement. If the new variable’s value is not nil (like Python’s None), the if block is executed. If the variable’s value is nil, then the block is ignored:

let responseMessages = [
    200: "OK",
    403: "Access forbidden",
    404: "File not found",
    500: "Internal server error"
]

let response = 444
if let message = responseMessages[response] {
    // This statement won't be run because message is nil
    print("Message: " + message)
}

Benefits

There are a lot of benefits to using the walrus operator in your code. But don’t take my word for it! Here’s what the authors of the idea said in their proposal:

Naming the result of an expression is an important part of programming, allowing a descriptive name to be used in place of a longer expression, and permitting reuse.

PEP 572 – Assignment Expressions

Let’s take a look at some examples.

Don’t Repeat Yourself

With the walrus operator, you can more easily stick to the DRY principle and reduce how often you repeat yourself in code. For example, if you want to print an error message if a list is too long, you might accidentally get the length of the list twice:

my_long_list = list(range(1000))

# You get the length twice!
if len(my_long_list) > 10:
    print(f"List is too long to consume (length={len(my_long_list)}, max=10)")

Let’s use the walrus operator to only find the length of the list once and keep that length inside the scope of the if statement:

my_long_list = list(range(1000))

# Much better :)
if (count := len(my_long_list)) > 10:
    print(f"List is too long to consume (length={count}, max=10)")

In the code block above, count := len(my_long_list) assigns the value 1000 to count. Then, the if statement is evaluated as if len(my_long_list) > 10. The walrus operator has two benefits here:

  1. We don’t calculate the length of a (possibly large) list more than once
  2. We clearly show a reader of our program that we’re going to use the count variable inside the scope of the if statement.

Reuse Variables

Another common example is using Python’s regular expression library, re. We want to look at a list of phone numbers and print their area codes if they have one. With a walrus operator, we can check whether the area code exists and assign it to a variable with one line:

import re

phone_numbers = [
    "(317) 555-5555",
    "431-2973",
    "(111) 222-3344",
    "(710) 982-3811",
    "290-2918",
    "711-7712",
]

for number in phone_numbers:
    # The regular expression "\(([0-9]{3})\)" checks for a substring
    # with the pattern "(###)", where # is a 0-9 digit
    if match := re.match("\(([0-9]{3})\)", number):
        print(f"Area code: {match.group(1)}")
    else:
        print("No area code")

Legible Code Blocks

A common programming pattern is performing an action, assigning the result to a variable, and then checking the result:

result = parse_field_from(my_data)
if result:
    print("Success")

In many cases, these types of blocks can be cleaned up with a walrus operator to become one indented code block:

if result := parse_field_from(my_data):
    print("Success")

These blocks can be chained together to convert a nested check statements into one line of if/elif/else statements. For example, let’s look at some students in a dictionary. We need to print each student’s graduation date if it exists, or their student id if available:

sample_data = [
    {"student_id": 200, "name": "Sally West", "graduation_date": "2019-05-01"},
    {"student_id": 404, "name": "Zahara Durham", "graduation_date": None},
    {"student_id": 555, "name": "Connie Coles", "graduation_date": "2020-01-15"},
    {"student_id": None, "name": "Jared Hampton", "graduation_date": None},
]

for student in sample_data:
    graduation_date = student["graduation_date"]
    if graduation_date:
        print(f'{student["name"]} graduated on {graduation_date}')
    else:
        # This nesting can be confusing!
        student_id = student["student_id"]
        if student_id:
            print(f'{student["name"]} is currently enrolled with ID {student_id}')
        else:
            print(f'{student["name"]} has no data")

With walrus operators, we can put the graduation date and student id checks next to each other, and better show that we’re checking for one or the other for each student:

sample_data = [
    {"student_id": 200, "name": "Sally West", "graduation_date": "2019-05-01"},
    {"student_id": 404, "name": "Zahara Durham", "graduation_date": None},
    {"student_id": 555, "name": "Connie Coles", "graduation_date": "2020-01-15"},
    {"student_id": None, "name": "Jared Hampton", "graduation_date": None},
]

for student in sample_data:
    # Much cleaner
    if graduation_date := student["graduation_date"]:
        print(f'{student["name"]} graduated on {graduation_date}')
    elif student_id := student["student_id"]:
        print(f'{student["name"]} is currently enrolled with ID {student_id}')
    else:
        print(f'{student["name"]} has no data")

Wrap Up

With walrus operators and named expressions, we can dramatically increase the legibility of our code by simplifying statements, reusing variables, and reducing indentation. For more great examples, check out the original proposal and the Python 3.8 release notes.

AWS CLI: Multiple Named Profiles

The AWS CLI supports named profiles so that you can quickly switch between different AWS instances, accounts, and credential sets. Let’s assume you have two AWS accounts, each with an access key id and a secret access key. The first account is your default profile, and the second account is used less often.

Adding a Named Profile

First, open ~/.aws/credentials (on Linux & Mac) or %USERPROFILE%\.aws\credentials (on Windows) and add your credentials:

[default]
aws_access_key_id=AKIAIOSFODNN7EXAMPLE1
aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY1

[user2]
aws_access_key_id=AKIAI44QH8DHBEXAMPLE2
aws_secret_access_key=je7MtGbClwBF/2Zp9Utk/h3yCo8nvbEXAMPLEKEY2

If your two profiles use different regions, or output formats, you can specify them in ~/.aws/config (on Linux & Mac) or %USERPROFILE%\.aws\config (on Windows):

[default]
region=us-west-2
output=json

[profile user2]
region=us-east-1
output=text

Note: do not add profile in front of the profile names in the credentials file, like we do above in the config file.

Most AWS CLI commands support the named profile option --profile. For example, verify that both of your accounts are set up properly with sts get-caller-identify:

# Verify your default identity
$ aws sts get-caller-identity

# Verify your second identity
$ aws sts get-caller-identity --profile user2

EKS and EC2 commands also support the --profile option. For example, let’s list our EC2 instances for the user2 account:

$ aws ec2 describe-instances --profile user2

Setting a Profile for Kubeconfig

The AWS CLI --profile option can be used to add new clusters to your ~/.kubeconfig. By adding named profiles, you can switch between Kubernetes contexts without needing to export new AWS environment variables.

If your EKS instance is authenticated with only your AWS access key id and access key secret, add your cluster with eks update-kubeconfig:

$ aws eks update-kubeconfig --name EKS_CLUSTER_NAME --profile PROFILE

If your EKS instance uses an IAM Role ARN for authentication, first copy the role ARN from the AWS Console: Go to the EKS service page, then Clusters, then select your cluster name, and find the IAM Role ARN at the bottom of the page. The format of the role ARN is typically arn:aws:iam::XXXXXXXXXXXX:role/role_name. Then, use eks update-kubeconfig:

aws eks update-kubeconfig --name EKS_CLUSTER_NAME --role-arn ROLE_ARN --profile PROFILE

To verify that your kubeconfig is set properly, use kubectx to switch to one of your new clusters and try to list out its services:

$ kubectx EKS_CLUSTER_NAME
Switched to context "EKS_CLUSTER_NAME".

$ kubectl get services
...

How to Use Jekyll on macOS Catalina with RVM

Apple bundles a system version of the Ruby programming language on macOS. Because system Ruby is used by the inner workings of the operating system, this version is not meant to be upgraded or modified by a user. With the Ruby Version Manager RVM, you can install an additional Ruby version for personal use.

Similar to pyenv, you can install multiple versions of Ruby with RVM and change the version you’re using on the fly. You can also install gems without sudo.

Installing RVM and Ruby

Before downloading RVM, first install gpg and the mpapis public key:

$ brew install gnupg
$ gpg --keyserver hkp://ipv4.pool.sks-keyservers.net --recv-keys xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

The keys (xxxx...) change often, so you will need to copy the most recent ones from the RVM install page.

Next, download the most recent stable version of RVM:

$ \curl -sSL https://get.rvm.io | bash -s stable --ruby

After installation, RVM will tell you to either open a new terminal or source rvm, so run the command it prints:

$ source ~/.rvm/scripts/rvm

You will also want to add rvm to your ~/.zshrc or ~/.bashrc to load when you open a terminal:

# Add this to your ~/.zshrc or ~/.bashrc
[[ -s "$HOME/.rvm/scripts/rvm" ]] && . "$HOME/.rvm/scripts/rvm"

Use rvm list to find a Ruby version you want to install, then tell RVM which version to use:

$ rvm list
$ rvm use 2.7.0

You can then verify that you’re using an RVM-managed version of Ruby:

$ which ruby
~/.rvm/rubies/ruby-2.7.0/bin/ruby

Installing Jekyll

First, verify you’re using a Ruby version managed by RVM in the above step. Then, install the Jekyll gem:

$ gem install jekyll bundler

If you’re already in a Jekyll website repo (or any folder with a Rakefile), you can use bundle to install your remaining requirements:

$ bundle install

You may then need to update Jekyll for your Rakefile requirements:

$ bundle update jekyll

Now you can up the Jekyll server:

$ bundle exec jekyll serve

Now check out your site at http://localhost:4000! See the Jekyll Quickstart for more details on starting a Jekyll blog.