Module 3: Collections and Iteration
Dictionaries and Sets: Addressing Data by Name
Storing a record as named fields instead of positions, reading fields that may be absent without crashing, and using sets for membership and de-duplication.
Lesson 11 of 46 in the recommended order · About 25 min (estimate)
On this page
Outcome
By the end of this lesson you can store an opportunity as a dictionary of named fields, read a field that may legitimately be absent without crashing, and use a set to answer "is this one of ours?" and "have we seen this already?".
Why it matters
Up to now a record has been a scatter of separate names: notice_id, agency, estimated_value. That works for one record and collapses immediately for forty. A dictionary keeps the fields of one record together, addressed by name, so a record can be passed around, counted, and stored as a single thing.
Named access also removes an entire class of silent bug. Reading position 3 of a row gives you whatever happens to be there after someone reorders the columns; reading record["response_due"] either gives you the response date or fails loudly.
Concept
A dictionary maps keys to values, written in braces with key: value pairs. Keys are usually strings; values can be anything, including other dictionaries and lists.
Two ways to read a field, and the difference is the whole point:
record["agency"]is strict. If the key is absent it raisesKeyErrorimmediately.record.get("agency")is lenient. If the key is absent it returnsNone, or a default you supply:record.get("set_aside", "unknown").
Choose strict for a field your program genuinely cannot proceed without, so the failure surfaces at the record that caused it. Choose lenient for a field a publisher legitimately omits, and supply a default that says so. Reaching for get everywhere out of caution hides real data problems; reaching for brackets everywhere turns an optional field into a crash.
Writing a field is record["set_aside"] = "8(a)", which adds the key if it is not there and replaces the value if it is. "agency" in record asks whether a key exists, without reading it. del record["agency"] removes it.
A set is an unordered collection of unique values, written in braces without colons: {"a", "b"}. It answers two questions well. Membership, value in allowed, is fast no matter how large the set is. Uniqueness is automatic: adding a value that is already present changes nothing, so set(ids) gives you the distinct identifiers and len(set(ids)) tells you whether the original list had duplicates.
Sets have no order and no positions, so allowed[0] is an error. If you need order, you need a list.
Read the code
record = {
"notice_id": "DLA-2026-0088",
"agency": "Defense Logistics Agency",
"estimated_value": 410000,
"set_aside": None,
}
recognised = {"Total Small Business", "8(a)", "SDVOSB"}
print(record["notice_id"])
print(record.get("response_due", "not published"))
print(record.get("set_aside", "unknown"))
print("agency" in record)
print(record["set_aside"] in recognised)
One record with four fields and a set of the categories this program understands.
The two get calls look similar and are doing different things. response_due is genuinely absent from the dictionary, so the default is used. set_aside is present, and its value is None, so get returns that None and the default is ignored entirely. A default only applies to a missing key, never to a key whose value happens to be empty. That distinction catches almost everyone once.
Predict the output
Predict all five printed lines. The last two are the interesting ones.
Check your prediction
DLA-2026-0088
not published
None
True
False
Line 3 is None, not unknown, for the reason above: the key exists. Line 4 is True because in on a dictionary tests keys, and agency is one. Line 5 is False because None is not a member of the recognised set, which is correct but unhelpful: it does not distinguish "an unrecognised category" from "no category at all", which is exactly the distinction Module 2 insisted on.
Modify the code
Change the fourth field from "set_aside": None to "set_aside": "8(a)", and predict lines 3 and 5.
What changes, and why
Line 3 becomes 8(a) and line 5 becomes True.
Line 3 changed because the value changed, not because the lookup changed. That is worth noticing: the "unknown" default in that get call has still never been used, and never will be while the key is present. If you want "unknown" to appear for a None value as well as an absent key, get alone cannot do it; you need an explicit check, which Module 4 turns into a small named function.
Debug the bug
An assistant was asked to count the distinct agencies in a batch and report the total. It produced this and said it prints 3.
records = [
{"notice_id": "GSA-2026-0731", "agency": "General Services Administration"},
{"notice_id": "DLA-2026-0088", "agency": "Defense Logistics Agency"},
{"notice_id": "SPE-2026-0412", "agency": "General Services Administration"},
]
agencies = set()
for record in records:
agencies.add(record["agency"])
print(len(records))
What's actually wrong
It prints 3, and 3 is the wrong answer to the question asked. There are only two distinct agencies; the General Services Administration appears twice.
The set was built correctly and then never used. The last line measures records, the original list, instead of agencies, the distinct values. Changing len(records) to len(agencies) prints 2.
This is wrong output in its purest form: no error, a plausible number, and a stated expectation that happened to match the wrong value. The general defence is to check that every name you built is actually read afterwards. A collection that is populated and never used is nearly always a symptom, not a leftover.
Try it yourself
One record that is missing a field publishers often omit, and a set of the categories this program recognises. Read one present field, one absent field, and one membership question.
Loading this exercise…
Practical challenge (optional)
Optional: build a list of five notice identifiers that contains one duplicate, then print both len(ids) and len(set(ids)). Write one sentence explaining what the difference between those two numbers tells a reviewer about the batch. In the capstone this exact comparison becomes a data-quality check, because a repeated identifier usually means the same notice was ingested twice.
Sign in to track your progress on this exercise.
AI collaboration
Checkpoint
Given record = {"id": "A-1", "value": 0, "notes": ""}:
- What does
record.get("value", 100)return? - What does
record.get("owner", "none")return? - What does
"notes" in recordreturn? - What does
bool(record["notes"])return? - What does
len({"a", "b", "a"})return?
Answers
0. The key exists, so the default is ignored, even though the value is falsy."none". The key does not exist, so the default is used.True.intests keys, and the key is present regardless of its value.False. The empty string is falsy, which is why "is the key there" and "does the field have content" are two different questions.2. A set discards the duplicate as it is built.
Sign in to track your progress on this exercise.
Summary and next step
A record is a dictionary of named fields, strict and defaulted lookups answer different questions, a default never applies to a present-but-empty value, and sets handle membership and uniqueness. Next: looping over many records to filter, count, and produce the assistant's first real shortlist.