Programming with Python › Control Flow and Collections › Day 53
Hands-on lab — Day 53: Dictionaries in Depth
- ← Back to the Day 53 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/programming-with-python/day-053-dictionaries-in-depth/
Commands
Setup
cd labs/sections/programming-with-python/day-053-dictionaries-in-depth
python3 --version Run
python3 examples/wordstats.py "the cat sat on the mat the cat"
python3 examples/wordstats.py --records
python3 starter/wordstats.py "one two two three three three" Test
bash tests/run_tests.sh File tree
examples/wordstats.py expected-output/FIELDS.md expected-output/sample-run.txt expected-output/test-run.txt metadata.yml README.md requirements/README.md security.md starter/wordstats.py tests/run_tests.sh troubleshooting.md
Lab README
Day 053 lab — Word Frequency & Records
Lesson
- Lesson title: Dictionaries in Depth
- Day number: 53 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-053-dictionaries-in-depth
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-053-dictionaries-in-depthwhen the site is running.
Purpose
Day 53's lesson teaches dictionaries in depth. This lab makes the idioms
concrete: you build Word Frequency & Records, a small program that
(a) counts word frequencies from text using the d.get(key, 0) + 1 idiom and
finds the most common word, and (b) stores small records as a list of
dictionaries, then groups them with setdefault and filters them with a dict
comprehension. You build it from a starter, one exercise at a time, then run an
automated test suite that checks real behaviour — including safe access, the
insertion-order guarantee, and tie-breaking. The word counter is the exact
machinery underneath text search and the term-frequency features of classical
language models.
Learning objectives
- Count word frequencies with the
.get()-with-default idiom, never aKeyError. - Find the most common item, breaking ties toward the first-seen key.
- Store structured data as a list of dictionaries and group it with
setdefault. - Build a filtered view with a dict comprehension.
- Validate input at the boundary and fail with a clear message and a non-zero exit code.
- Prove a function is importable and testable — the payoff of the main guard from Day 49.
Prerequisites
- The Day 53 lesson (read it first — it explains every pattern this lab uses).
- Days 43-52: a working Python 3 install plus variables, strings, numbers, input/output, reading errors, assembling a small program, and lists.
- A text editor and a terminal. No programming experience beyond this course is assumed.
Supported operating systems
- macOS — fully supported (tested on macOS with Apple Silicon, Python 3.14).
- Linux — fully supported (any distribution with Python 3.7+ and bash).
- Windows — use WSL and follow the Linux path, or substitute
pythonforpython3if that is how Python is exposed. The program is pure standard-library Python and behaves identically everywhere Python 3.7+ runs.
Hardware requirements
Any computer that runs Python 3. The program builds a few small dictionaries in memory; it needs no special memory, disk, or GPU.
Required software
python3(3.7 or newer; tested on 3.14). Version 3.7+ matters: the lab relies on the dictionary insertion-order guarantee introduced then.bashfor the test runner (preinstalled on macOS and Linux).- Standard library only — the
sysmodule ships with Python. No packages to install. Seerequirements/README.md.
Free and open-source options
Everything here is free and open source: Python, bash, and the standard library. No account, API key, network access, or purchase is needed.
Installation
None beyond Python itself. Move into this directory and you are ready:
cd labs/sections/programming-with-python/day-053-dictionaries-in-depth
python3 --version # confirm Python 3.7+ is available
File structure
day-053-dictionaries-in-depth/
├── README.md ← you are here
├── metadata.yml ← machine-readable lab metadata
├── starter/
│ └── wordstats.py ← YOUR working file (4 numbered exercises)
├── examples/
│ └── wordstats.py ← complete reference implementation
├── tests/
│ └── run_tests.sh ← automated checks (counting, records, imports)
├── expected-output/
│ ├── sample-run.txt ← real captured run of the reference
│ ├── test-run.txt ← real captured run of the test suite
│ └── FIELDS.md ← required behaviour on every platform
├── requirements/
│ └── README.md ← dependency statement (Python 3.7+ only)
├── troubleshooting.md
└── security.md
How to run
From this directory:
## 1. See the finished program first: counting, then records
python3 examples/wordstats.py "the cat sat on the mat the cat"
python3 examples/wordstats.py --records
python3 examples/wordstats.py # no argument: prints an error, exits non-zero
## 2. Your task: complete the four exercises in the starter, then run it
python3 starter/wordstats.py "one two two three three three"
## 3. Prove the module is importable (the payoff of the main guard)
python3 -c "import sys; sys.path.insert(0, 'examples'); from wordstats import count_words; print(count_words('a b a'))"
## 4. Check your work
bash tests/run_tests.sh
What the commands do
python3 examples/wordstats.py "the cat sat on the mat the cat"— runs the complete reference: it counts the words withcount_words(theget()-with-default idiom), prints each word with its count in insertion order, and prints the most common word (most_common, ties going to the first seen).python3 examples/wordstats.py --records— runs the records half: it groups the built-inPEOPLErecords by role withgroup_by_role(usingsetdefault) and builds a filtered, sorted view of names with a dict comprehension (names_up_to_m).python3 examples/wordstats.pywith no argument — prints a clear usage error to standard error and exits with code 1.python3 starter/wordstats.py ...— runs your version. The starter ships with four exercises stubbed out (each raisingNotImplementedErroruntil you finish it): count with.get(), find the top word, group withsetdefault, and build a dict comprehension.python3 -c "...count_words..."— imports one function from the module and calls it, without running the whole program. This works only because the main guard holdsmainback on import.bash tests/run_tests.sh— runs the reference on counting and records inputs (checking output and exit code), imports two functions to check their return values, and checks your starter (structurally until you finish, strictly afterwards). Exits 0 only if every check passes.
Expected output
See expected-output/sample-run.txt — a real
captured run:
$ python3 examples/wordstats.py "a a b"
a: 2
b: 1
most common: a (2)
$ python3 examples/wordstats.py --records
engineers: ['Ada', 'Alan']
admirals: ['Grace']
names A-M: ['Ada', 'Alan', 'Grace']
The counts print in insertion order (the order each word first appeared),
not alphabetically — the Python 3.7+ guarantee. The program is deterministic,
so your output will match exactly.
expected-output/FIELDS.md lists the required
behaviour for every input on every platform.
Validation steps
- Run
python3 examples/wordstats.py "a a b"— it must printa: 2,b: 1, andmost common: a (2). - Run
python3 examples/wordstats.py --records— it must print the two role groups and thenames A-Mline. - Run
python3 examples/wordstats.py; echo $?— it must print a usage error and then1. - Complete the four exercises in
starter/wordstats.py, then run it on the same inputs and confirm it matches the reference. - Run the tests (next section) — every check must pass.
Tests
bash tests/run_tests.sh
Expected final line while the starter is unfinished: 12 checks, 0 failure(s). Once you complete all four starter exercises, six more checks run
your version through the same counting and records inputs plus the main-guard
check, giving 18 checks, 0 failure(s). The command exits 0 on success and
non-zero on any failure, so it can run in CI. A full captured run is in
expected-output/test-run.txt.
Cleanup
Nothing to clean up: the program and tests read only their command-line
arguments and write nothing outside their own console output (no files, no
network, no settings). To reset your work, restore the starter from git:
git checkout -- starter/wordstats.py.
Troubleshooting
See troubleshooting.md for the full list: python vs
python3, the deliberate NotImplementedError stubs, KeyError while
counting, unhashable type on a bad key, insertion order versus sorting,
tie-breaking, and importing vs running.
Security notes
See security.md. Short version: the program makes no network
calls, writes no files, and needs no privileges. It validates input at the
boundary, never eval()s it, and relies on Python's default hash randomisation
(leave it on) to resist hash-flooding attacks.
Extension exercises
- Rewrite the counting and grouping with
collections.Counterandcollections.defaultdictand confirm the output is identical; note in a comment what each helper removed (Counter(...).most_common(1)should replace your top-word logic). - Add a timing experiment that counts a large repeated string with a
dictionary versus scanning a list of
[word, count]pairs, and record the two timings — you should see the list version grow far slower as the vocabulary grows (the O(1)-versus-O(n) difference made visible). - Build a toy vocabulary: a dict comprehension mapping each distinct word to a unique integer id, then use it to turn a sentence into a list of ids — the token-to-id step that turns text into the numbers a language model consumes.
Navigation
- Previous day: Day 52 — Lists in Depth
(
labs/sections/programming-with-python/day-052-lists-in-depth/). - Next day: Day 54 — continues Week 8, Data Structures
(
labs/sections/programming-with-python/day-054-.../, to be written). - Week 8 project: builds on the collection idioms of this week — counting, grouping, and querying structured data with dictionaries.
Expected output
FIELDS.md
# Expected output — Day 053 lab
This directory holds real captured runs from the authoring machine
(macOS, Apple Silicon, Python 3.14, 2026-07-13). Your numbers will match
exactly, because the program is deterministic — the same input always
produces the same output on every platform.
## Files
- `sample-run.txt` — the reference program run on several inputs (counting,
records, a bad-input error) plus the `python3 -c` import check.
- `test-run.txt` — a full run of `bash tests/run_tests.sh` with the starter
still unfinished (12 checks, 0 failures).
## Required behaviour on every platform
A correct program must, for these inputs, produce exactly:
| Command | Standard output | Exit code |
| ------- | --------------- | --------- |
| `wordstats.py "a a b"` | `a: 2` / `b: 1` / `most common: a (2)` | 0 |
| `wordstats.py "the cat sat on the mat the cat"` | `the: 3` first (insertion order), then `cat: 2`, `sat: 1`, `on: 1`, `mat: 1`, `most common: the (3)` | 0 |
| `wordstats.py --records` | `engineers: ['Ada', 'Alan']` / `admirals: ['Grace']` / `names A-M: ['Ada', 'Alan', 'Grace']` | 0 |
| `wordstats.py` (no argument) | (stderr) `error: expected text to count, or --records` | 1 |
Two details are load-bearing and identical on every platform:
- **Insertion order.** Counts print in the order each word first appeared,
not alphabetically — this is the Python 3.7+ guarantee. `the` prints before
`cat` because it was seen first.
- **Tie-breaking.** `most common` breaks ties in favour of the word seen
first, because the ranking uses a strictly-greater comparison.
The only platform difference is the shell prompt shown before each command
(`$` here); the program's own output is identical everywhere Python 3.7+ runs.
## Test-suite counts
- With the starter unfinished: `12 checks, 0 failure(s).`
- Once you complete all four starter exercises: `18 checks, 0 failure(s).`
(the six extra checks run your starter through the same counting and records
inputs as the reference, plus a check that it has the main guard).
sample-run.txt
$ python3 examples/wordstats.py "the cat sat on the mat the cat"
the: 3
cat: 2
sat: 1
on: 1
mat: 1
most common: the (3)
$ python3 examples/wordstats.py "a a b"
a: 2
b: 1
most common: a (2)
$ python3 examples/wordstats.py --records
engineers: ['Ada', 'Alan']
admirals: ['Grace']
names A-M: ['Ada', 'Alan', 'Grace']
$ python3 examples/wordstats.py ; echo "exit: $?"
error: expected text to count, or --records
usage: python3 wordstats.py "some words" | --records
exit: 1
$ python3 -c "import sys; sys.path.insert(0, 'examples'); from wordstats import count_words; print(count_words('a b a'))"
{'a': 2, 'b': 1}
test-run.txt
Testing <repo>/labs/sections/programming-with-python/day-053-dictionaries-in-depth/examples/wordstats.py ...
ok: a a b -> a: 2
ok: a a b -> most common a
ok: counts in insertion order
ok: records: engineers group
ok: records: admirals group
ok: records: A-M comprehension
ok: no argument rejected
Testing importability of examples/wordstats.py ...
ok: import count_words('a b a') == {'a': 2, 'b': 1}
ok: import most_common ties to first seen
Testing starter/wordstats.py ...
ok: starter is valid Python
Note: starter/wordstats.py still has unfinished exercises — testing structure only.
ok: starter defines count_words
ok: starter defines group_by_role
12 checks, 0 failure(s).
Source files
examples/wordstats.py (4041 bytes)
#!/usr/bin/env python3
"""Word Frequency & Records — dictionaries in depth.
A complete, small program that shows the core dictionary idioms:
* counting word frequencies with the d.get(key, 0) + 1 pattern
* finding the most common word (ties broken by first appearance)
* storing small records as a list of dictionaries
* grouping records with setdefault
* building a filtered view with a dict comprehension
It reads input from the command line (never input()), validates it, prints
clear output, and fails gracefully with a non-zero exit code on bad input.
Usage:
python3 wordstats.py "some words to count"
python3 wordstats.py --records
Examples:
python3 wordstats.py "a a b" -> a: 2 / b: 1 / most common: a (2)
python3 wordstats.py --records -> grouped and filtered records
"""
import sys
# A tiny table of records, modelled as a list of dictionaries. Each record is
# a dict with named fields — exactly the shape of a JSON payload or a config.
PEOPLE = [
{"name": "Ada", "role": "engineer"},
{"name": "Grace", "role": "admiral"},
{"name": "Alan", "role": "engineer"},
]
def count_words(text):
"""Return a dict mapping each word in text to how many times it appears.
Uses the get()-with-default idiom so a first-seen word starts at 0 and no
KeyError is ever raised. Insertion order (Python 3.7+) means the returned
dict lists words in the order they first appeared.
"""
counts = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
return counts
def most_common(counts):
"""Return (word, count) for the highest count; ties go to the first seen.
Raises ValueError if counts is empty, so callers get a clear message
rather than a confusing result.
"""
if not counts:
raise ValueError("no words to rank")
best_word = None
best_count = -1
for word, n in counts.items():
if n > best_count: # strictly greater, so the earliest word wins ties
best_word, best_count = word, n
return best_word, best_count
def group_by_role(people):
"""Group record names by their role using setdefault.
Returns a dict mapping each role to the list of names with that role, with
roles in the order they were first encountered.
"""
groups = {}
for person in people:
groups.setdefault(person["role"], []).append(person["name"])
return groups
def names_up_to_m(people):
"""Return the sorted names whose first letter is A-M, via a dict comprehension.
Builds a {name: role} mapping with a comprehension, then filters and sorts
the names — showing a dict comprehension feeding a downstream query.
"""
by_name = {person["name"]: person["role"] for person in people}
return sorted(name for name in by_name if name[0].upper() <= "M")
def report_counts(text):
"""Print the word counts and the most common word for text."""
counts = count_words(text)
if not counts:
raise ValueError("no words found in the input text")
for word, n in counts.items():
print(f"{word}: {n}")
word, n = most_common(counts)
print(f"most common: {word} ({n})")
def report_records():
"""Print the grouped and filtered records."""
groups = group_by_role(PEOPLE)
for role, names in groups.items():
print(f"{role}s: {names}")
print(f"names A-M: {names_up_to_m(PEOPLE)}")
def main(argv):
"""Entry point. Returns an exit code: 0 on success, 1 on bad input."""
args = argv[1:]
if not args:
print("error: expected text to count, or --records", file=sys.stderr)
print('usage: python3 wordstats.py "some words" | --records',
file=sys.stderr)
return 1
try:
if args[0] == "--records":
report_records()
else:
report_counts(args[0])
except ValueError as err:
print(f"error: {err}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
metadata.yml (712 bytes)
lesson_id: D053
day: 53
kind: python-program
languages: [python]
setup_commands:
- cd labs/sections/programming-with-python/day-053-dictionaries-in-depth
- python3 --version
run_commands:
- python3 examples/wordstats.py "the cat sat on the mat the cat"
- python3 examples/wordstats.py --records
- python3 starter/wordstats.py "one two two three three three"
test_commands:
- bash tests/run_tests.sh
cleanup_commands:
- 'git checkout -- starter/wordstats.py # optional: reset your work'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-13'
executed_on: 'macOS (Apple Silicon), Python 3.14.0, bash tests/run_tests.sh → 12 checks, 0 failure(s), exit 0'
requirements/README.md (947 bytes)
# Dependencies — Day 053 lab
**Python 3 only. No third-party packages.**
- `python3` (3.7 or newer; tested on 3.14). The insertion-order guarantee this
lab relies on is a language feature since 3.7. Preinstalled on most Linux
distributions and installable on macOS; you set this up on Day 43.
- `bash` for the test runner (preinstalled on macOS and Linux).
- Only the Python standard library is used — specifically the `sys` module,
which ships with Python. The extension exercises optionally use
`collections` (also standard library). There is deliberately no
`requirements.txt`: a dictionary lab should run on a plain Python install
with nothing to install first.
Check your Python is present and new enough:
```bash
python3 --version
```
If that prints `Python 3.7` or higher, you are ready. Windows users: run the
commands inside WSL, or use `python` in place of `python3` if that is how
Python is exposed on your system.
starter/wordstats.py (3727 bytes)
#!/usr/bin/env python3
"""Word Frequency & Records — YOUR working file.
Build this program one exercise at a time. Each numbered exercise below names
exactly what to write. The finished reference is in examples/wordstats.py —
try each exercise yourself before peeking.
When all exercises are done, this file should behave just like the reference:
python3 starter/wordstats.py "a a b" -> a: 2 / b: 1 / most common: a (2)
python3 starter/wordstats.py --records -> grouped and filtered records
Then run: bash tests/run_tests.sh
"""
import sys
PEOPLE = [
{"name": "Ada", "role": "engineer"},
{"name": "Grace", "role": "admiral"},
{"name": "Alan", "role": "engineer"},
]
def count_words(text):
"""Return a dict mapping each word in text to how many times it appears."""
# Exercise 1: COUNT WITH .get().
# Split text into words, then for each word do:
# counts[word] = counts.get(word, 0) + 1
# so a first-seen word starts at 0 (no KeyError). Return the counts dict.
# Verify by hand: count_words("a b a") must be {"a": 2, "b": 1}.
raise NotImplementedError("Exercise 1: implement count_words with .get()")
def most_common(counts):
"""Return (word, count) for the highest count; ties go to the first seen."""
# Exercise 2: FIND THE TOP WORD.
# If counts is empty, raise ValueError("no words to rank").
# Otherwise walk counts.items() and keep the word with the largest count.
# Use a STRICTLY-greater test (n > best_count) so the EARLIEST word wins
# on ties. Return (best_word, best_count).
raise NotImplementedError("Exercise 2: implement most_common")
def group_by_role(people):
"""Group record names by their role using setdefault."""
# Exercise 3: GROUP WITH setdefault.
# For each person dict, append person["name"] to the list stored under
# person["role"], creating that list once with:
# groups.setdefault(person["role"], []).append(person["name"])
# Return the groups dict.
raise NotImplementedError("Exercise 3: implement group_by_role with setdefault")
def names_up_to_m(people):
"""Return the sorted names whose first letter is A-M, via a dict comprehension."""
# Exercise 4: DICT COMPREHENSION.
# Build a {name: role} mapping with a dict comprehension over people, then
# return the sorted names whose first letter (name[0].upper()) is <= "M".
raise NotImplementedError("Exercise 4: implement names_up_to_m with a dict comprehension")
def report_counts(text):
"""Print the word counts and the most common word for text. (Provided.)"""
counts = count_words(text)
if not counts:
raise ValueError("no words found in the input text")
for word, n in counts.items():
print(f"{word}: {n}")
word, n = most_common(counts)
print(f"most common: {word} ({n})")
def report_records():
"""Print the grouped and filtered records. (Provided.)"""
groups = group_by_role(PEOPLE)
for role, names in groups.items():
print(f"{role}s: {names}")
print(f"names A-M: {names_up_to_m(PEOPLE)}")
def main(argv):
"""Entry point. Returns an exit code: 0 on success, 1 on bad input. (Provided.)"""
args = argv[1:]
if not args:
print("error: expected text to count, or --records", file=sys.stderr)
print('usage: python3 wordstats.py "some words" | --records',
file=sys.stderr)
return 1
try:
if args[0] == "--records":
report_records()
else:
report_counts(args[0])
except ValueError as err:
print(f"error: {err}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
tests/run_tests.sh (4245 bytes)
#!/usr/bin/env bash
# Tests for the Day 053 lab. Run from the lab directory:
# bash tests/run_tests.sh
#
# Exercises the complete reference program (examples/wordstats.py) on known
# inputs, checking both the printed output and the process exit code, then
# imports functions from the module and checks their return values. Finally it
# checks the learner's starter: structurally while exercises are unfinished,
# and to the same strict standard once they are complete.
# No network, non-interactive. Exits 0 only if every check passes.
set -u
# Keep the working tree clean: do not let imported modules write __pycache__.
export PYTHONDONTWRITEBYTECODE=1
lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ref="${lab_dir}/examples/wordstats.py"
starter="${lab_dir}/starter/wordstats.py"
failures=0
checks=0
check() {
local label="$1" ok="$2"
checks=$((checks + 1))
if [ "${ok}" = "yes" ]; then
echo " ok: ${label}"
else
echo " FAIL: ${label}"
failures=$((failures + 1))
fi
}
# check_run <label> <script> <expect_exit> <needle> <arg...>
# Runs the program, checks its exit code equals expect_exit and that its
# combined output contains needle.
check_run() {
local label="$1" script="$2" expect_exit="$3" needle="$4"
shift 4
local out code
out="$(python3 "${script}" "$@" 2>&1)"
code=$?
if [ "${code}" -eq "${expect_exit}" ] && printf '%s' "${out}" | grep -qF "${needle}"; then
check "${label}" "yes"
else
check "${label}" "no"
echo " (exit ${code}, expected ${expect_exit}; output: ${out})"
fi
}
run_program_checks() {
local script="$1"
echo "Testing ${script} ..."
# Counting: correct counts, insertion order, most-common line, exit 0.
check_run "a a b -> a: 2" "${script}" 0 "a: 2" "a a b"
check_run "a a b -> most common a" "${script}" 0 "most common: a (2)" "a a b"
check_run "counts in insertion order" "${script}" 0 "the: 3" "the cat sat on the mat the cat"
# Records: grouping with setdefault + dict comprehension, exit 0.
check_run "records: engineers group" "${script}" 0 "engineers: ['Ada', 'Alan']" --records
check_run "records: admirals group" "${script}" 0 "admirals: ['Grace']" --records
check_run "records: A-M comprehension" "${script}" 0 "names A-M: ['Ada', 'Alan', 'Grace']" --records
# Bad input: no argument -> clear error, non-zero exit.
check_run "no argument rejected" "${script}" 1 "expected text to count"
}
# --- Reference program: always tested strictly ---
run_program_checks "${ref}"
# --- Import functions and check return values (main-guard payoff) ---
echo "Testing importability of examples/wordstats.py ..."
if python3 -c "import sys; sys.path.insert(0, '${lab_dir}/examples'); \
from wordstats import count_words; \
assert count_words('a b a') == {'a': 2, 'b': 1}"; then
check "import count_words('a b a') == {'a': 2, 'b': 1}" "yes"
else
check "import count_words('a b a') == {'a': 2, 'b': 1}" "no"
fi
if python3 -c "import sys; sys.path.insert(0, '${lab_dir}/examples'); \
from wordstats import most_common; \
assert most_common({'x': 2, 'y': 2}) == ('x', 2)"; then
check "import most_common ties to first seen" "yes"
else
check "import most_common ties to first seen" "no"
fi
# --- Learner starter ---
echo "Testing starter/wordstats.py ..."
if python3 -c "compile(open('${starter}').read(), '${starter}', 'exec')" 2>/dev/null; then
check "starter is valid Python" "yes"
else
check "starter is valid Python" "no"
fi
if grep -q 'NotImplementedError' "${starter}"; then
echo "Note: starter/wordstats.py still has unfinished exercises — testing structure only."
grep -q 'def count_words' "${starter}" && check "starter defines count_words" "yes" || check "starter defines count_words" "no"
grep -q 'def group_by_role' "${starter}" && check "starter defines group_by_role" "yes" || check "starter defines group_by_role" "no"
else
# Learner finished: hold the starter to the same strict standard.
run_program_checks "${starter}"
grep -q '__name__ == "__main__"' "${starter}" && check "starter has the main guard" "yes" || check "starter has the main guard" "no"
fi
echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]
Troubleshooting
Troubleshooting — Day 053 lab
python: command not found
Use python3 explicitly, as every command in this lab does. On macOS and
most Linux systems, bare python may be missing or point to an old version.
Check with python3 --version — you need 3.7 or newer for the insertion-order
behaviour this lab relies on.
The starter raises NotImplementedError when I run it
That is expected until you finish the exercises. Each unfinished function
raises NotImplementedError on purpose so you cannot accidentally think an
empty function "works." Replace each raise NotImplementedError(...) line with
the real body described in the comment above it. Once all four exercises are
done, the file runs like the reference.
KeyError while counting
You read counts[word] before the key exists. The very first time a word is
seen, its key is not in the dictionary yet, so the square-bracket read raises
KeyError. Use the idiom instead:
counts[word] = counts.get(word, 0) + 1
counts.get(word, 0) returns the running count, or 0 for a first-seen word,
so no key ever has to be created by hand and no KeyError is raised.
TypeError: unhashable type: 'list'
You tried to use a list (or a set, or another dict) as a dictionary key. Keys must be hashable, meaning immutable — a string, a number, or a tuple of those. Use one of those as the key. In this lab the keys are words and roles (strings), so this error usually means a variable holds the wrong thing.
Counts come out in the wrong order
A plain dictionary preserves insertion order (the order words first
appeared), not alphabetical order. In this lab that is exactly what you want:
the prints before cat because it was seen first. If you sorted the output,
remove the sort; if you want alphabetical order for some other purpose, add
sorted(...) explicitly — the two are different on purpose.
most common is wrong when two words tie
The rule is that the word seen first wins a tie. That falls out of using a
strictly-greater test (n > best_count) as you scan: an equal count never
displaces the earlier word. If you used >=, a later word would wrongly win.
ModuleNotFoundError: No module named 'wordstats'
Python imports a module by looking on its search path (sys.path), which does
not include the examples/ subfolder by default. The import one-liner in this
lab adds it first:
python3 -c "import sys; sys.path.insert(0, 'examples'); from wordstats import count_words; print(count_words('a b a'))"
Run this from the lab directory (the folder that contains examples/), not
from inside examples/ itself.
Importing the file runs the whole program
This is what the main guard prevents. If importing your module prints output or
exits, you either removed if __name__ == "__main__": or wrote
program-running code at the top level instead of inside main. Only the
guarded sys.exit(main(sys.argv)) should trigger execution — that is what lets
a test import count_words without launching the program.
bash: tests/run_tests.sh: Permission denied
Run it through bash explicitly (as the README shows) rather than executing it
directly: bash tests/run_tests.sh. You do not need to chmod +x anything.
Security notes
Security notes — Day 053 lab
-
What the program does: reads one command-line argument (text to count, or the
--recordsflag), builds dictionaries in memory, and prints the result. It makes no network connections, reads and writes no files, and changes no settings. The test runner is equally self-contained and non-interactive. -
Validate input at the boundary. The program checks that an argument was supplied and that the text contains at least one word, printing a clear error to standard error and exiting non-zero otherwise — never a raw
KeyErroror traceback. When you build dictionaries from untrusted input (a parsed JSON request, say), do the same: never assume an incoming payload has the keys you expect. Used.get(key, default)or checkkey in dfirst, exactly as this lab teaches. -
Never
eval()input to "parse" it. As on Day 49, the rule is absolute: turn text into data with safe operations (str.split(),int(),float()), never by executing it.eval()andexec()run their argument as Python code and must never touch input. -
Hash randomisation is a feature — leave it on. Python randomises string hashing on each run by default to resist "hash-flooding" attacks, where an attacker sends keys engineered to collide into one bucket and slow your dictionary from O(1) toward O(n). You get this protection for free; do not disable it (
PYTHONHASHSEED) and do not rely on the exact numeric value ofhash()staying the same across runs. -
Fail loudly, not silently. On bad input the program prints a clear message to standard error and exits with a non-zero code. Silently computing a wrong answer from bad input is worse than a crash, because no one notices.
-
Privileges: everything runs as your normal user. Nothing here needs
sudo. Readexamples/wordstats.pyandtests/run_tests.shbefore running them — every file in this lab is short enough to read first.