Dictionary Comprehensions

Concepts covered: Dict Comprehension

A dict comprehension builds a whole dict in one expression: {key_expr: value_expr for item in iterable if condition}. Python runs the for over the iterable, skips items where the optional if is false, evaluates the key and value expressions for each remaining item, and inserts the pair. The result is always a new dict, and the source is left as it was.

row = {"Order_ID": "A-1001", "Amount": "42.50", "Coupon": None, "Status": "paid"}

renamed = {key.lower(): value for key, value in row.items()}
present = {key: value for key, value in renamed.items() if value is not None}
typed = {key: float(value) if key == "amount" else value for key, value in present.items()}
print(typed)

These 3 shapes cover most daily use: transform keys, filter pairs, transform values. An if ... else inside the value expression is a ternary and picks a value for every item; an if after the for is a filter and drops items. Putting if ... else after the for is a SyntaxError. Filtering this way is also how to remove keys from a dict: build the smaller dict instead of deleting while looping.

Any iterable, not only dicts

The source can be anything iterable. Pairing a CSV header with a row of values goes through zip, and indexing a list of records by id reads each record for both the key and the value.

header = ["order_id", "customer", "amount"]
values = ["A-1001", "c-17", "42.5"]
record = {col: val for col, val in zip(header, values)}

orders = [
    {"order_id": "A-1001", "status": "paid"},
    {"order_id": "A-1002", "status": "paid"},
    {"order_id": "A-1001", "status": "refunded"},
]
by_id = {o["order_id"]: o["status"] for o in orders}
print(record)
print(by_id)

A-1001 appears twice and the dict holds only refunded. A comprehension inserts pairs in iteration order, and a repeated key overwrites the earlier value while keeping its first slot. For a stream of change events that is deduplication keeping the latest state; anywhere else it is silent data loss. dict(zip(header, values)) builds the same record and is the shorter choice when nothing needs transforming or filtering.

Where a comprehension stops

Each item produces 1 assignment and the last one per key wins, so a comprehension cannot collect several items under a key; that stays a setdefault loop. It also cannot read the dict it is building or catch an exception per item, and when the logic needs either, a plain loop is clearer. Values it passes through untouched are the source's own objects, so {k: v for k, v in record.items()} shares nested lists with record the same way .copy() does.

Inverting a lookup with a comprehension inherits the overwrite rule.

codes = {"GB": "United Kingdom", "UK": "United Kingdom", "DE": "Germany"} is inverted with names = {name: code for code, name in codes.items()}. What does names["United Kingdom"] hold?

The habits worth keeping from all 5 sections:

Do
  • Loop with .items() when the body needs both key and value
  • Take list(d) before indexing a view or changing keys mid-loop
  • Merge with DEFAULTS | overrides, highest priority on the right
  • Read nullable levels with (d.get(k) or {})
  • Copy each nested level a function changes, or deepcopy the record
Avoid
  • Don't update or |= a module-level defaults dict
  • Don't expect a merge to combine nested dicts; it replaces them
  • Don't treat new = old or .copy() as a full copy of nested data
  • Don't group with a comprehension; repeated keys keep only the last
  • Don't write row = row.update(...); update returns None

About this lesson

This section is part of the Dictionaries: Intermediate lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.