Cover Image

Dict Moves in Python

681 words. Time to Read: About 6 minutes.

Quick tip time!

Today, I started the #100DaysOfCode challenge again (for the millionth time). I’m determined to actually succeed at this challenge, and I refuse to give up. This time, I’m using the Python Bytes Code Challenges website and their 100 days project suggestions. During today’s challenge, I learned a neat little trick for working with dictionaries that I wanted to share.

The Challenge

The challenge is this: go through a dictionary of words, which is really just a copy of /usr/share/dict/words. Find the word that scores the highest in Scrabble, using these letter scores:

SCRABBLE_SCORES = [
  (1, "E A O I N R T L S U"),
  (2, "D G"),
  (3, "B C M P"),
  (4, "F H V W Y"), 
  (5, "K"), 
  (8, "J X"), 
  (10, "Q Z"),
]
LETTER_SCORES = {
    letter: score for score, letters in scrabble_scores
    for letter in letters.split()
}
# {"A": 1, "B": 3, "C": 3, "D": 2, ...}

The Issue

The issue is that I don’t want to worry about whether or not there are any invalid characters in the input (for now at least). So if I look up the word “snoot!43@@@ “, right now, I’d prefer to see the score for SNOOT and then 0 points for the rest of the characters. I know there are a bunch of ways to do this, but the first way that popped into my head was to use a default of 0 (i.e. if you try to look up a character that’s not in LETTER_SCORES, it returns zero instead of raising a KeyError.)

Enter DefaultDict

Luckily for us, Python comes with exactly the thing we need: a defaultdict, courtesy of the standard library’s collections module. Its usage is reasonably straightforward: you supply the defaultdict with a class or function that constructs the default if the input isn’t found. Let me show you.

from collections import defaultdict

zeros = defaultdict(int)
zeros["a"] = 1
zeros["b"] = zeros["definitely not in there"] + 4
print(zeros)
# => defaultdict(<int>, {"a": 1, "b": 4, "definitely not in there": 0})

Since the zeros dict can’t find the "definitely not in there" key, it calls its default-maker function, int. Go ahead and open up your Python REPL and try just calling the int function with no arguments.

>>> int()
0

The int function, called with no arguments, returns 0 every time.

You can even create your own default-maker functions (and classes will work too)!

from random import choice

def confusing_default():
    possibles = ["1", 1, True, "banana"]
    return choice(possibles)

tricky_dict = defaultdict(confusing_default)
tricky_dict["Ryan"]
# => "banana"
tricky_dict["Python"]
# => True
tricky_dict["Why would you do this?"]
# => 1
tricky_dict
# => defaultdict(<confusing_default>, {"Ryan": "banana", "Python": True, "Why would you do this?": 1})

Often times, you can do things a little quicker with lambdas.

from random import randint

SCREAMING = defaultdict(lambda: "A")
for i in range(20):
    key = randint(0, 3)
    SCREAMING[key] += "A"
SCREAMING
# => defaultdict(<function <lambda> at 0x108707f28>, {0: 'AAAAAAAA', 1: 'AAAAAAA', 3: 'AAAAA', 2: 'AAAA'})

In fact, I actually think that using defaultdict(lambda: 0) is more explicit and less confusing than using defaultdict(int), as long as you’re not creating huge numbers of these defaultdicts this way.

Upgrading to a DefaultDict

Now, finally, we’re ready for the quick tip. Up above, I defined LETTER_SCORES as a plain, old Python dict. How do I get the default behaviors I want, quickly? One way is using the built-in dict.update() function, which merges two dictionaries.

FORGIVING_SCORES = defaultdict(lambda: 0)
FORGIVING_SCORES.update(LETTER_SCORES)

FORGIVING_SCORES["Q"]
# => 10

FORGIVING_SCORES["@"]
# => 0

Hooray!

Granted, this isn’t a perfect solution, because the FORGIVING_SCORES defaultdict stores each of the invalid asks. It’s probably OK if you’re not expecting a huge number of invalid look-ups. If you are worried about staying space-efficient, though, it’s probably better to do this:

score = LETTER_SCORES.get("@") or 0

The get function returns None if a KeyError occurs, and the or allows us to provide a sane default if the lookup goes bad. And everybody’s happy!

Author: Ryan Palo | Tags: python tricks | Buy me a coffee Buy me a coffee

Like my stuff? Have questions or feedback for me? Want to mentor me or get my help with something? Get in touch! To stay updated, subscribe via RSS