
A scene from "The Walrus and the Carpenter", by Lewis Carroll, drawn by Sir John Tenniel in 1871.
Python 3.8¹ introduced an assignment expression operator :=, which is called a walrus. The operator allows you to assign variables within expressions, such as if statements, for loops, or list comprehensions. So where two lines were required before, one will do now. This is a short article about how I've started using Walrus with SqlAlchemy.
SqlAlchemy is a Python SQL library that works as an expression language. You can chain Python functions to create SQL commands or as an Object Relation Mapper (ORM), where Python classes define a mapping between their attributes and a table's columns or an expression in SQL.
You can compose a select expression like this:
As you can see, when chaining functions, we have an expression language in Python that almost represents the SQL language itself.
Here's an example of an ORM query:
The ORM is equally expressive, but by taking advantage of Python classes, it reduces wordiness.
After that, let's have some more ORM magic:
This will insert both the user and their role. This one statement is the equivalent of these two in the expression language:
And with the walrus?
Here, Role('admin') creates an instance that receives its primary key as the session commits. The session uses this object again to let Janet be an admin. In the past, I would have had to create an admin role on its own line.
While writing a unit test, I found myself creating the following basic data:
It feels like I've said what I want to happen, and the ORM does the work for me. I can think inline, but I don't have to go back and ensure I've prepared all the pre-conditions — I can reuse what I created earlier! Suddenly, the ORM is starting to feel a lot more like the expression language: 'These are the instructions I want you to consume, and as we step through, can you please remember this object for later use?'
Like a pipe operator, the walrus allows us to think as we do in natural languages. Read this and weep:
p = load(transform(extract()))
instead of this:
p = Process() | extract() | transform() | load()
asyncio.run(p())
If we didn't have classes, we could use generators to encapsulate state and function. The pipe operator encourages this. By overloading the or operator, which is implemented with the __or__ method, we can construct an asynchronous process that chains the elements together by turning each into asynchronous generators. The arguments to the function become initialisation/configuration, then each function call becomes an asend, and the result is yielded. That's another story.
Before we end, I should mention that the walrus comes with a health warning:
There are many times where it's possible for you to use the walrus operator, but it won't necessarily improve the readability or efficiency of your code. In those cases, you're better off writing your code in a more traditional manner.²
You might also be tempted to beat the scope by placing the walrus in a bracket. For example: (foo:=3). You'll find that, more often than not, this will end in a syntax error. You can't use it with attribute assignments or chaining initializations. Having said that, I'm happy to use it to make my unit tests more readable and in small functions where inline makes it more readable.
Object-oriented or functional? Welcome to the walrus. It's blurring the distinction and lowering the barricades with its teeth. Quite a lot for such a little operator, so keep it close at hand³.