NOETRION

Choose a topic

DAY 15 OF 30 · TRANSFORM AND ORGANIZE

Dictionary comprehensions

Build mappings, audit duplicate keys, and choose collision policies instead of silently losing records.

90–120 minutes suggested study timePython 3.12Not yet completed

Today's outcomes

  • Construct a mapping from key-value pairs.
  • Predict replacement when keys repeat.
  • Compare comprehensions with explicit accumulation.

Before you start: Complete Day 14 and its practical challenge.

Understand the concept

A dictionary comprehension builds a mapping

A dictionary comprehension has a key expression and value expression separated by a colon, followed by iteration and optional filtering. {topic: minutes for topic, minutes in pairs} maps each topic to a minute count. As with a list comprehension, the resulting outer dictionary is new.

The mapping's keys must be hashable and unique in the result. If several input pairs produce the same key, later assignments replace the earlier value. The comprehension does not keep every input record simply because it inspected every record. Its output count can be smaller than the number of admitted inputs.

The same issue appears when deriving normalized keys. "Lists" and " lists " may become one key under a trim-and-casefold rule. That can be useful for a summary, but replacement is not the same as adding the observations together. Choose whether repeated keys should be rejected, summed, grouped, or deliberately replaced.

Transforming values is different from aggregating records

A one-to-one value transformation is a good comprehension use: convert every known minute total to hours, or keep only selected fields from a record. Aggregation carries state across several observations and may be clearer as an ordinary loop.

For example, a report by topic might need to sum 30 minutes and 45 minutes under the same normalized topic key. A plain mapping comprehension retains only the later value 45. Both programs can return a plausible dictionary, but only the accumulation loop implements the intended total of 75.

Filtering must preserve valid boundary values

Filtering with if minutes excludes zero, not just None. Use an explicit missing-value check when zero is valid. Also keep label policy separate from numeric validity: a blank topic label and a negative duration are two different rejection reasons.

Today's accepted numerical inputs are nonnegative integers or None. The function will count blank labels and missing observations as ignored under its specific contract. Day 17 adds stricter validation for arbitrary external values. Do not broaden today's function promises beyond the documented input domain.

Worked example

pairs = [("lists", 30), ("functions", 60), ("lists", 45)]
last_value = {topic: minutes for topic, minutes in pairs}

totals = {}
for topic, minutes in pairs:
    totals[topic] = totals.get(topic, 0) + minutes

hours = {topic: minutes / 60 for topic, minutes in totals.items()}
assert last_value == {"lists": 45, "functions": 60}
assert totals == {"lists": 75, "functions": 60}
assert hours == {"lists": 1.25, "functions": 1.0}
print("Last-value mapping:", last_value)
print("Accumulated totals:", totals)
Policy Result for lists Interpretation
Later value replaces earlier value 45 Latest assignment under this input order
Sum every observation 75 Total minutes for the topic
Preserve a list of observations 30 and 45 History available for later analysis

There is no universally correct collision policy. A current-status lookup might intentionally use a verified latest record, while a measurement history should retain every observation. “Keep last” only represents the latest chronological record if source order actually follows the intended time rule.

Guided practice

  1. Predict: build a comprehension from [('A', 1), ('A', 2), ('B', 3)]. Compare its output count with its input count and identify the overwritten value.
  2. Modify: normalize the topic keys before accumulating. With ('Lists', 30) and (' lists ', 45), expect one normalized key totaling 75.
  3. Reconstruct: transform the accumulated totals into formatted hour strings using a dictionary comprehension. Keep the numeric totals separately so display formatting does not destroy data needed for further calculations.

For each mapping transformation, ask whether keys can collide. Checking only the final total may miss that one record was replaced by another with the same amount. Retain counts or an audit when reconciliation matters.

Daily challenge

Implement totals_by_topic(records). Each record has topic text and minutes containing a nonnegative integer or None. Trim and casefold topics. Ignore records with a blank topic or missing minutes, preserve a known zero, and sum repeated normalized topics. Return the totals mapping and ignored_count.

Record Contribution
Lists, 30 lists gains 30
lists, 45 lists gains another 45
Functions, 0 functions exists with total zero
blank topic, 60 ignored
Strings, None ignored

Expect {'lists': 75, 'functions': 0} and ignored_count 2. For an empty input, expect an empty dictionary and zero ignored records. The challenge requires accumulation, so do not force the entire calculation into a comprehension.

Reference solution

Compare after attempting the challenge
def totals_by_topic(records):
    totals = {}
    ignored = 0
    for record in records:
        key = record["topic"].strip().casefold()
        minutes = record["minutes"]
        if not key or minutes is None:
            ignored += 1
            continue
        totals[key] = totals.get(key, 0) + minutes
    return totals, ignored

fixture = [
    {"topic": "Lists", "minutes": 30},
    {"topic": " lists ", "minutes": 45},
    {"topic": "Functions", "minutes": 0},
    {"topic": " ", "minutes": 60},
    {"topic": "Strings", "minutes": None},
]
assert totals_by_topic(fixture) == ({"lists": 75, "functions": 0}, 2)
assert totals_by_topic([]) == ({}, 0)
print(totals_by_topic(fixture))

The get default initializes a topic's additive total to zero. That is appropriate for an accumulation baseline; it is not converting a missing individual observation into zero. The missing-observation branch still excludes None explicitly.

Debug and explain

Symptom Investigate Useful check
Fewer records than expected Duplicate or normalized keys Compare admitted count and distinct keys
Total equals the final observation Replacement used instead of addition Two records under one key
Zero-only topic disappears Truth-value filter One known-zero record
Mapping grows unpredictably Inconsistent normalization Mixed spelling fixture

Explain why this solution uses a loop for accumulation and why a comprehension is still appropriate for the later hours conversion. Give a domain where keeping the final value is sensible, and name the evidence needed to interpret “final” as “current.”

Review and next step

Save the collision example and accumulation fixture. Rebuild the policy from a written contract during your next review. Day 16 saves text to local files, making preservation and round-trip checks part of your workflow.

Sources and verification

Original teaching examples checked with Python 3.12.14. Library behavior reviewed against these primary references on October 3, 2026. Day 26's examples also use Beautiful Soup 4.14.3.

DAILY CHECKPOINT

Check your understanding

Five questions, 20 points each. Pass at 85% or above, which requires all five correct. Feedback explains each answer; revise the lesson and retry when needed.

01. What happens with duplicate output keys in a dict comprehension?

02. Why can a dict comprehension lose records?

03. When should aggregation use a loop instead?

04. Does creating a new dictionary prove nested values are copied?

05. What should you test after key normalization?

Review your practical work

Check these statements after running your own challenge. This is your self-review; the website does not execute or assess your Python code.

Pass the knowledge check and finish the practical self-review to save completion.