Organizing Data

Choosing and Checking a Representation


Learning Objectives

  • You can choose a representation by considering required information and operations.
  • You can distinguish original records, lookup tables, and derived summaries.
  • You can test a representation with duplicate names, empty data, and shared objects.
  • You can evaluate an AI-proposed design by identifying both its benefits and its costs.

A structure can be valid Python and still be awkward or incorrect for a task. Choosing a representation means checking what information must survive and which operations the program must support.

This is the same specification-first habit from Part 3, applied to data rather than only to a function body.

Begin with the Questions the Program Must Answer

Suppose a program records several study sessions. It must preserve their order, allow repeated subjects, and later calculate total minutes for each subject.

These are two different representations:

sessions = [
{"subject": "Programming", "minutes": 20},
{"subject": "Math", "minutes": 30},
{"subject": "Programming", "minutes": 15}
]
totals = {"Programming": 35, "Math": 30}

The first keeps individual sessions. The second keeps a summary. Both are useful, but they answer different questions.

From the summary alone, the program cannot recover whether Programming took one session of 35 minutes or several sessions. Summarizing deliberately discards detail. Do not substitute a summary for original records when later work still needs that detail.

When a program calculates a total, it uses the records as they are at that moment. Changing a record later does not change a saved total. If the program keeps both records and calculated totals, it must update the totals when the records change or calculate them again when needed.

Position and Identity Are Different Relationships

Word Clues used corresponding positions because a clue belongs to the character at that position. A grid uses row and column positions because location matters.

A catalogue is different. An item’s name, availability, and identifier belong together even when the catalogue is reordered. A record makes that relationship explicit.

Parallel lists can work, but every insertion, deletion, and reordering must keep them synchronized. A list of records groups fields that must move together.

This does not make dictionaries automatically better. A list of five die values remains clearer than a dictionary with keys named "first_die", "second_die", and so on.

State the Shape at the Function Boundary

A useful structured-data contract includes more than the outer type:

Function: available_names(items)
Input: a list of records with a unique "id" string, a "name" string, and an "available" boolean.
Result: a new list of names for available items, in input order. Repeated names are permitted.
Empty input: returns [].
Effects: no printing and no changes to the input list or records.

The uniqueness rule applies to identifiers, not names. Two physical cameras can share a name and still be different catalogue items.

Here is an implementation of this contract:

def available_names(items):
names = []
for item in items:
if item["available"]:
names.append(item["name"])
return names

Tests should preserve that distinction:

items = [
{"id": "E10", "name": "Camera", "available": True},
{"id": "E11", "name": "Camera", "available": True}
]
assert available_names(items) == ["Camera", "Camera"]

Deduplicating the names would violate this contract even though the result might look tidy.

When Two Views Serve Different Operations

One collection does not have to make every operation equally direct. A program may keep an ordered list as its primary collection and build a dictionary when it also needs frequent lookup by a stable identifier.

The two outer collections can contain references to the same records. This makes field updates visible through either view, but structural changes still need a policy: adding, removing, or replacing a record may require both collections to be updated or the derived view to be rebuilt.

Copies create a different responsibility. If the views contain separate records, the program must decide whether later changes should be synchronized. Nested mutable fields need their own copying decision as well.

The key must suit the lookup relationship. A field that describes a record but is not guaranteed to be unique cannot, by itself, identify exactly one record.

Build a lookup by identifier that shares the original item records:

Build an item lookup

0 / 15 points

Write item_lookup(items). Each item has a unique id string, a name string, and an available Boolean. Names may repeat. Return a new dictionary mapping each identifier to its existing item record. Do not copy the records: callers must be able to update the same item through the lookup. Do not print or change anything during the call. Empty input returns {}.

After lookup = item_lookup(items), changing lookup["E10"]["available"] must change that item’s record in items. Adding a new key to lookup must not append to items.

Then compare two proposed contact views, checking what their keys and copying choices allow a caller to do.

Review two contact views

0 / 10 points

A contact application must preserve a user-chosen display order, allow repeated names and several phone numbers, and frequently find a contact by its unique identifier. An AI assistant proposes keeping both of these views:

contacts = [
{"id": "C10", "name": "Sam Lee", "phones": ["555-0100"]},
{"id": "C11", "name": "Sam Lee", "phones": ["555-0101", "555-0102"]},
]
contacts_by_id = {}
for contact in contacts:
contacts_by_id[contact["id"]] = contact

Review the proposal. Explain one operation made direct by the list and one made direct by the dictionary. Explain what happens in the list if the program changes contacts_by_id["C10"]["name"], and why. Identify one consistency responsibility created by keeping both collections. Finally, use the repeated Sam Lee records to explain why {name: contact} would not meet the requirements.

You may recommend the two-view design or another design, but connect the recommendation to the stated operations and information.

Compare Flat and Grouped Records

A dictionary can also keep a list of records under each key:

visits_by_building = {
"Library": [{"time": "09:00"}, {"time": "12:00"}],
"Lab": [{"time": "10:00"}]
}

visits_by_building["Library"] retrieves that building’s list. To visit all the records, loop over the dictionary’s items and then over each list:

for building, visits in visits_by_building.items():
for visit in visits:
print(building, visit["time"])

This visits both Library records before the Lab record. The loop follows the groups; it does not arrange records from different groups into time order.

The next exercise makes this trade-off concrete with attendance data. As you compare the representations, notice which information comes from each record and which information is supplied by the group key.

Compare attendance records

0 / 20 points

A club keeps attendance records. You will write the same operations for two ways of storing them.

A flat list keeps one full record for each meeting and member:

records = [
{"member": "Ari", "date": "2026-04-08", "attended": False},
{"member": "Mina", "date": "2026-04-01", "attended": False},
{"member": "Ari", "date": "2026-04-01", "attended": True},
]

A grouped dictionary stores the member name as its key:

records_by_member = {
"Ari": [
{"date": "2026-04-08", "attended": False},
{"date": "2026-04-01", "attended": True},
],
"Mina": [
{"date": "2026-04-01", "attended": False},
],
}

All records have exactly the fields shown. Member names and dates are strings, and attendance values are Booleans. Dates are labels here: do not parse or sort them. Member names must match exactly, including case and spaces. A member’s grouped list may be empty. Repeated records are allowed and must remain repeated in the result.

1. Find records for a member

Define records_for_member(records, member) and records_for_member_grouped(records_by_member, member). Each returns all records for the requested member, preserving their order in that input. Return [] when the member is missing or has no records.

Both functions return full records with "member", "date", and "attended" fields. The grouped function must include the member name from the dictionary key in each returned record.

2. Find missed meetings

Define missed_records(records) and missed_records_grouped(records_by_member). Each returns all full records where "attended" is False. Return [] when there are none.

The flat function preserves the original list order. The grouped function visits members in dictionary insertion order and keeps the order within each member’s list. These orders can differ: grouping does not remember how records for different members were interleaved in the original list.

All four functions return lists without printing, reading input, or changing their inputs. The flat functions may reuse the original record dictionaries or return copies. The grouped functions create full records because their input records do not contain the member name. Work through both stages in the same file.

Validate at a Useful Boundary

Many examples in this part assume that records already satisfy their contracts. The code constructing those records is under our control.

In Part 5, a file may contain missing fields or values of the wrong type. That is a different boundary: the program must establish the contract before handing the records to ordinary calculation functions.

Check the incoming records before passing them to the calculation functions. Those functions can then use the agreed fields and types without repeating the same checks.

Check Your Understanding

  1. What information can be lost when individual records become a summary?
  2. When does positional correspondence express the actual relationship?
  3. Why might item names be unsuitable as unique identifiers?
  4. What should a structured-data contract say beyond “receives a list”?
  5. Why does a successful run not prove that a representation preserves the required information?