Dicts

Updated

September 8, 2026

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.py

Output:

['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.py

Output:

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.py

Output:

{'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.py

Output (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.
  • KeyError means the shape is wrong. Do not catch it to return None unless that is the API.

Try this

  1. In ticket_dict.py, del ticket["table"] then set ticket["table"] = 12 again. Print list(ticket) and notice table moved to the end.
  2. Merge three dicts: defaults | incoming | {"status": "fired"}.
  3. Change print_note to use get and print (no note) when missing. Re-run missing_note.py.