Dicts
Dicts
A dict maps keys to values. The boring default at the desk is a dict for a ticket that is still growing fields, d["id"] when the key must exist, d.get("note", "") when it might not, and | to merge overlays without mutating the originals.
Mental model
Keys must be hashable (int, str, tuple of hashables, frozenset — not list, not dict). Values can be anything. Lookup is by key, not by position.
Since Python 3.7, insertion order is part of the language. for key in d: walks keys in the order they were first inserted. Updating a key in place does not move it. Deleting and re-inserting puts it at the end.
d[k] raises KeyError if k is missing. d.get(k) returns None. d.get(k, default) returns the default. d.setdefault(k, []) inserts the default if missing and returns the value — handy, and easy to mutate a shared list if you are careless.
a | b is a new dict: keys from a, then keys from b overwrite. a |= b updates a in place. {**a, **b} is the older unpack form. Prefer | when both sides are dicts.
Worked examples
Case 1: Keys, order, update in place
Save as ticket_dict.py.
# ticket_dict.py
def main():
ticket = {"id": 7, "table": 12}
ticket["status"] = "open"
ticket["table"] = 3
print(list(ticket))
print(list(ticket.values()))
for key, value in ticket.items():
print(key, value)
if __name__ == "__main__":
main()Run:
uv run python ticket_dict.pyOutput:
['id', 'table', 'status']
[7, 3, 'open']
id 7
table 3
status open
table stayed in its original position after the update. status was appended. list(ticket) is the keys.
Case 2: get when a field is optional
# ticket_note.py
def note_line(ticket):
note = ticket.get("note", "")
if note:
return f"ticket {ticket['id']}: {note}"
return f"ticket {ticket['id']}: (no note)"
def main():
print(note_line({"id": 7, "note": "window"}))
print(note_line({"id": 8}))
print({"id": 8}.get("note"))
if __name__ == "__main__":
main()Run:
uv run python ticket_note.pyOutput:
ticket 7: window
ticket 8: (no note)
None
get("note") without a default is None, which is easy to confuse with a stored None. Prefer an explicit default when the rest of the function wants a string.
Case 3: Merge with |
# merge_ticket.py
def main():
defaults = {"status": "open", "cents": 0, "table": 0}
incoming = {"id": 7, "table": 12, "cents": 1850}
ticket = defaults | incoming
print(ticket)
override = {"status": "paid"}
print(ticket | override)
print(ticket)
if __name__ == "__main__":
main()Run:
uv run python merge_ticket.pyOutput:
{'status': 'open', 'cents': 1850, 'table': 12, 'id': 7}
{'status': 'paid', 'cents': 1850, 'table': 12, 'id': 7}
{'status': 'open', 'cents': 1850, 'table': 12, 'id': 7}
defaults | incoming keeps default key order, then adds new keys (id) at the end. incoming wins on cents and table. The last print shows | did not mutate ticket.
The trap
KeyError on a key you assumed was there. It is the right exception when the ticket is corrupt. It is a trap when the field is optional and you meant get.
# missing_note.py
def print_note(ticket):
print(ticket["note"])
def main():
print_note({"id": 8, "table": 3})
if __name__ == "__main__":
main()Run:
uv run python missing_note.pyOutput (the process exits non-zero):
Traceback (most recent call last):
File "missing_note.py", line 11, in <module>
main()
File "missing_note.py", line 7, in main
print_note({"id": 8, "table": 3})
File "missing_note.py", line 3, in print_note
print(ticket["note"])
KeyError: 'note'
Fix for optional: ticket.get("note", ""). Fix for required: let KeyError surface, or raise ValueError("ticket missing note") with the ticket id. Do not write if "note" in ticket and then silently skip a required field.
A second trap: d[k] += 1 also raises KeyError on a missing key. Use d[k] = d.get(k, 0) + 1 or collections.Counter.
The boring rule
- Required field:
d[k]. Optional field:d.get(k, default). - Merge overlays with
a | b. Do not mutate defaults. - Trust insertion order. Do not sort keys unless the output needs a sort.
- Keys are unique. A second write overwrites.
KeyErrormeans the shape is wrong. Do not catch it to returnNoneunless that is the API.
Try this
- In
ticket_dict.py,del ticket["table"]then setticket["table"] = 12again. Printlist(ticket)and noticetablemoved to the end. - Merge three dicts:
defaults | incoming | {"status": "fired"}. - Change
print_noteto usegetand print(no note)when missing. Re-runmissing_note.py.