Counting with a Dictionary
A list gets a value by its index: words[0], words[1], and so on.
A dictionary gets a value by a key that you choose, such as a letter or a name: codes["a"], codes["b"], and so on.
In this lab, you will build a dictionary in the Python shell, look things up in it, and see what goes wrong when a key isn't there.
Then you will write three short functions: one that looks up each character of a word, one that tallies how many times each character appears, and one that uses the tally to answer a question.
Save the files from this lab in your Week08 folder.
Warmup Exercise
Suppose you wanted to count how many times each vowel appears in a string.
With what you know so far, you would need five variables (a_count, e_count, and so on) and an if/elif with five branches.
How many variables would you need to count every character, including uppercase letters, digits, spaces, and punctuation?
A dictionary replaces all of those variables with one. Keep this question in mind until Step 3.
Keys and Values
A dictionary is written with curly braces. Each entry is a key, a colon, and the value stored under that key.
This dictionary has three entries.
The keys are "a", "b", and "c", and each value is a code word from the NATO phonetic alphabet, which pilots and radio operators use to spell words out loud without confusing letters like B and D.
Square brackets do three jobs, depending on where they appear:
| Code | What it does |
|---|---|
codes["b"] |
Get the value stored under the key "b". |
codes["d"] = "Delta" |
Set the value for "d", adding the key if it isn't there yet. |
codes["b"] += "!" |
Update the value for "b", which must already be there. |
The in operator asks a question:
| Code | What it does |
|---|---|
"b" in codes |
Check whether "b" is a key. The result is True or False. |
An empty dictionary is just {}, and len(codes) is the number of keys.
Dictionaries in type hints
The type hint for a dictionary names the type of its keys and the type of its values.
codes is a dict[str, str], and a dictionary of counts like {"n": 2, "o": 2} is a dict[str, int].
You will see both in this lab's starter code.
Predict in the Shell
Open a terminal in VS Code, and type python to start a Python shell.
Then type the lines below, one at a time.
Before you press Enter on each line, write down what you expect it to show: a value, nothing at all, or an error.
| Line | Code | Your prediction |
|---|---|---|
| 1 | codes["b"] |
|
| 2 | codes["B"] |
|
| 3 | codes["Bravo"] |
|
| 4 | "c" in codes |
|
| 5 | "Charlie" in codes |
|
| 6 | codes["d"] = "Delta" |
|
| 7 | codes |
|
| 8 | codes["a"] = "Alpha" |
|
| 9 | codes |
|
| 10 | len(codes) |
Now start a second dictionary, for counting:
| Line | Code | Your prediction |
|---|---|---|
| 11 | counts = {} |
|
| 12 | counts["x"] += 1 |
|
| 13 | counts["x"] = 1 |
|
| 14 | counts["x"] += 1 |
|
| 15 | counts |
Type exit() to return to the system shell when you're done.
Check your predictions
Lines 2, 3, and 12 each raise a KeyError, and lines 6, 8, 11, 13, and 14 show nothing, because an assignment doesn't produce a value.
Five observations worth pulling out of that table:
- Keys are matched exactly, so
"B"and"b"are different keys. Asking for a key that isn't there doesn't returnNoneor0; it crashes the program with aKeyError. - Lookup goes one way only: from a key to its value.
"Bravo"is a value, not a key, socodes["Bravo"]fails, and"Charlie" in codesisFalse.inasks about the keys. - Setting a key that isn't there yet adds it (line 6). Setting a key that is already there replaces its value (line 8), and the dictionary still has four keys, not five.
+=gets the old value, adds to it, and sets the result. With no old value to get, line 12 fails, which is the sameKeyErroras line 2.- So a count has to start with
=and grow with+=. That's whyinmatters: it tells you which of the two to do.
(The official spelling really is "Alfa", by the way, so that speakers of every language pronounce it the same way.)
Step 1: Getting Started
Download the following files into your Week08 folder.
(Right click each link, and "Save link as…")
Open characters.py.
At the top is a dictionary named NATO with all 26 letters, and below it are three functions for you to write.
Run pytest on test_characters.py.
The tests should run and fail, because the functions are not written yet.
Write the loops yourself
Python has shortcuts that do this lab's work for you, like a dictionary's get() method and the built-in Counter class.
You will see these and other features in Week 11.
For now, use only the four operations from the tables above, the same ones PA2 allows.
Step 2: spell
The spell() function looks up each character of a word in NATO, and returns a list of what it found.
For example, spell("cab") returns ["Charlie", "Alfa", "Bravo"].
Start with an empty list.
Then loop through the word, one character at a time, and append() the code word for each character.
Not every character is a key of NATO, though.
spell("r2d2") has to handle the "2", and from the shell you know that NATO["2"] would crash.
So check first: if the character is a key, append its code word; otherwise, append the character itself, unchanged.
That gives ["Romeo", "2", "Delta", "2"].
Run pytest until both test_spell functions pass.
Then answer this question: what does spell("Bob") return, and why isn't the first entry "Bravo"?
Step 3: char_counts
This is the main function of the lab, and the answer to the warmup.
The char_counts() function returns a dictionary that maps each character of a string to the number of times it appears.
For example, char_counts("noon") returns {"n": 2, "o": 2}.
Here is the plan, in four steps:
- Start with an empty dictionary named
counts. - Loop through the text, one character at a time.
- If the character is already a key of
counts, add 1 to its count. Otherwise, this is the first time you've seen it, so set its count to 1. - After the loop, return
counts.
Before you write any code, trace the plan by hand for char_counts("noon").
Write down the whole dictionary after every pass of the loop.
The first row is done for you.
| Pass | ch |
Already a key? | counts after the pass |
|---|---|---|---|
| 1 | "n" |
No | {"n": 1} |
| 2 | |||
| 3 | |||
| 4 |
Now write the function, and run pytest until test_char_counts passes.
Break it on purpose
Make each change below, one at a time, and predict what pytest will report before you run it. Put the code back the way it was after each one.
- Delete the
if, theelse, andcounts[ch] = 1, leaving onlycounts[ch] += 1in the loop. - Delete the
if, theelse, andcounts[ch] += 1, leaving onlycounts[ch] = 1in the loop. - Move
counts = {}inside the loop, as its first line.
One of these crashes, and the other two run but return the wrong dictionary. Which test assertion is the first to catch each one?
Step 4: is_anagram
Two words are anagrams if they're made of exactly the same characters, like "listen" and "silent", or "night" and "thing".
The order of the characters doesn't matter, but how many of each does.
First, try this in the Python shell:
Two dictionaries are equal when they have the same keys with the same values, no matter what order the entries were added in.
So is_anagram() doesn't need a loop of its own.
Call char_counts() on each word, and compare the two dictionaries.
The whole function body is one line.
Run pytest until every test passes.
Then look at the last three assertions in test_is_anagram.
Why is "aab" not an anagram of "abb", even though both words use only the letters a and b?
And why is "Act" not an anagram of "cat"?
Submission
Upload only characters.py to Gradescope.
The autograder runs the provided test_characters.py.