Locked learning resources

Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Locked learning resources

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Simpler Updating of Dictionaries

00:00 In the previous episode, I introduced you to the zoneinfo library. In this episode, I’m going to be talking about the new operators for dictionaries.

00:09 Python 3.9 introduces two new operators for dictionaries. The first is the pipe operator, allowing you to create a new dictionary based on the merging of two existing dictionaries.

00:22 The second is the pipe equals operator that acts like the .update() function call. Let me start by showing you the old way of doing things. In order to do that, I’m going to need a couple of dictionaries. This first one is PyCon events, and the second one is EuroPython events.

00:46 You can create new dictionaries using the double asterisks operator (**). This example creates a third dictionary called merged, which constitutes pycon and europython. pycon is untouched, europython is untouched, and merged is the mix of the two of them.

01:09 Another way of doing this is to start with the pycon dictionary and copy it. merged is now an exact copy of pycon, and then iterate through europython, adding it to merged.

01:33 This results in the same thing. You could also have achieved this with the .update() function. But keep in mind, .update() operates on the existing dictionary, so now pycon has both the original and the europython entries inside of it.

01:51 There’s yet another way of doing that, so let me reset pycon.

02:00 Combining the ideas from before, I can use the .copy() and .update() methods together.

02:13 Except—that doesn’t work. And the reason that doesn’t work is because the .update() function operates in place—it does not return a dictionary.

02:21 So pycon.copy() gets run, the result of that dictionary has .update() called upon it, and then .update() returns nothing. So this short form doesn’t work.

02:32 You can tackle this problem using the recently introduced walrus operator (:=).

02:43 Pay close attention to where the parentheses are in this example. The first set of parentheses surround the walrus operator. (merged := pycon.copy()) calls pycon.copy(), returns the result into merged, and then the walrus operator returns the value of merged into the expression.

03:02 So the inside of the parentheses is the value of merged after the .copy(). .update() is then run on that value. You can see the result here. Just because you’ve got a result doesn’t mean it was pretty getting there. Python 3.9 has introduced the pipe operator to make cleaner-looking code to solve the same kind of problem.

03:25 First up, merge pycon and europython. This is returning a new dictionary, not touching pycon or europython. There’s pycon. See? It’s been untouched.

03:38 So, the pipe operator acts as a merge. The pipe equals operator acts as an update. This time, pycon has been updated to include both the values it had before, as well as the values from europython.

03:55 These operators have also been implemented on dictionary-like objects. Let me show you an example.

04:06 This is the defaultdict. A defaultdict is just like a dictionary, but instead of raising a KeyError when something isn’t found, you can set a value to be returned instead.

04:18 Let me just set up a couple of examples.

04:30 The lambda here tells the defaultdict that if there is a KeyError, to return the result of the lambda—in this case, just an empty string.

04:39 Let me set up another one so I can do some merging.

04:48 Now, if I use the double asterisk operator (**) from before, it’ll work, but it won’t still be a defaultdict. The result is a regular dictionary. Fortunately, the pipe operator is aware of the class.

05:05 Unlike the other mechanism, with the double asterisk operator, the pipe operator actually returns a defaultdict with the merged results. The operators are also aware of other ways of constructing dictionaries.

05:19 Let me just build out another example for you, listing off some modules inside of Python.

05:34 And now I can or-equals the kinds of things that you put in a dictionary constructor, like a list of tuples. The end result is an updated dictionary. This gives you power as to how you can update your dictionary, and still do it with clean-looking code.

05:53 The order of operation on the pipe operator is important. Here’s a new example with some places.

06:07 Notice that both of these dictionaries have a key called "Georgia". If I merge asia with usa first, the dictionary in the second part, i.e. the usa in this case, overwrites the keys in the first part.

06:23 So "Georgia" in the merged dictionary is "Atlanta". If I do this the other way around, this time, asia overwrites the usa.

06:33 The order of operations on your operator changes the resulting values. The pipe and pipe equal operators have been added to most of the dictionary-like objects.

06:46 The only exception is they haven’t been added to the abstract base classes. Like many operators in Python, these have been implemented using dunder methods: .__or__() and .__ior()__, for pipe and pipe equals, respectively. Because these are implemented as functions, you can override them and change the behavior yourself.

07:08 For more information on these operators, you can see the original PEP. Next up, I’ll show you the changes to the decorator syntax.

Avatar image for rivet

rivet on Oct. 10, 2020

What happens if the default value of each defaultdict is not the same when you use the merge operator? Does the first default value take precedence?

Avatar image for Christopher Trudeau

Christopher Trudeau RP Team on Oct. 11, 2020

Yes, the value on the left of the pipe operator is what is merged into:

>>> from collections import defaultdict
>>> cats = defaultdict(lambda:"meow")
>>> cats["small"]
'meow'
>>> dogs = defaultdict(lambda:"woof")
>>> dogs["large"]
'woof'
>>> cats
defaultdict(<function <lambda> at 0x7fbf3da27af0>, {'small': 'meow'})
>>> dogs
defaultdict(<function <lambda> at 0x7fbf3da480d0>, {'large': 'woof'})
>>> cats | dogs
defaultdict(<function <lambda> at 0x7fbf3da27af0>, {'small': 'meow', 'large': 'woof'})
>>> mixed = cats | dogs
>>> mixed["parrot"]
'meow'

You can see the default value of “meow” in the mixed defaultdict, or you can also see the address of the original lambda associated when you print the defaultdict itself out: <function <lambda> at 0x7fbf3da27af0, which appears in both cats and mixed.

Become a Member to join the conversation.