Skip to main content
Learning Center
Python Programming

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 raises KeyError immediately.
  • record.get("agency") is lenient. If the key is absent it returns None, 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": ""}:

  1. What does record.get("value", 100) return?
  2. What does record.get("owner", "none") return?
  3. What does "notes" in record return?
  4. What does bool(record["notes"]) return?
  5. What does len({"a", "b", "a"}) return?
Answers
  1. 0. The key exists, so the default is ignored, even though the value is falsy.
  2. "none". The key does not exist, so the default is used.
  3. True. in tests keys, and the key is present regardless of its value.
  4. False. The empty string is falsy, which is why "is the key there" and "does the field have content" are two different questions.
  5. 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.

learning.goultergroup.com

The interactive parts of this page have not loaded. Reading and links still work; reload the page to try again.