Python · Data Structures

Python Dictionaries Explained — Ordering, Lookups, and Merging

4 min read Updated 2026-06-19

Practice Dictionaries interview questions

Python dictionaries, explained

The dict is the workhorse of Python — it backs objects' attributes, keyword arguments, JSON, and countless lookups. It's also a reliable interview topic because the questions probe real understanding: why lookups are O(1), why keys must be hashable, and what changed about ordering in Python 3.7. This guide walks through the behaviour that matters.

O(1) lookups via hashing

A dict is a hash table: it computes hash(key) to decide which bucket a value goes in, so lookups, inserts, and deletes are average O(1) regardless of size. That's why a dict crushes a list for "does this key exist?" — a list is O(n).

prices = {"apple": 30, "banana": 10}
prices["apple"]          # O(1) — hash to the bucket, no scanning
"banana" in prices       # O(1) membership test

The cost is memory (buckets and spare capacity) and that keys must be hashable — the mechanism that makes O(1) possible is the same one that constrains what can be a key.

Keys must be hashable

A key's hash must stay constant for its lifetime, so keys must be immutable/hashable: strings, numbers, tuples, frozensets — but not lists, dicts, or sets.

{(1, 2): "point"}        # tuple key — fine
{[1, 2]: "point"}        # TypeError: unhashable type: 'list'

If a mutable key could change after insertion, its hash would change and the value would be unfindable in its original bucket. Use a tuple instead of a list, a frozenset instead of a set.

Insertion ordering (since 3.7)

As of Python 3.7, dicts preserve insertion order as a language guarantee (it was a CPython implementation detail in 3.6). Iterating a dict yields keys in the order they were added.

d = {}
d["z"] = 1
d["a"] = 2
list(d)          # ['z', 'a'] — insertion order, not sorted

This is why collections.OrderedDict is rarely needed now — though it still has uses (order-sensitive equality and move_to_end). To sort, do it explicitly: dict(sorted(d.items())).

get vs and setdefault

Indexing a missing key raises KeyError. dict.get(key, default) returns a default instead, and setdefault(key, default) returns the existing value or inserts and returns the default — handy for accumulating.

counts = {}
for word in ["a", "b", "a"]:
    counts[word] = counts.get(word, 0) + 1     # no KeyError on first sight
counts            # {'a': 2, 'b': 1}

groups = {}
for name in ["ann", "amy", "bob"]:
    groups.setdefault(name[0], []).append(name)  # insert [] if missing, then append
groups            # {'a': ['ann', 'amy'], 'b': ['bob']}

For counting and grouping, collections.Counter and collections.defaultdict are cleaner still — but get/setdefault are the built-in tools to know.

Merging dictionaries

Since Python 3.9 the | operator merges dicts; |= updates in place. Before that, {**a, **b} unpacking does the same. In all of them, the right-hand dict wins on conflicting keys.

a = {"x": 1, "y": 2}
b = {"y": 9, "z": 3}

a | b            # {'x': 1, 'y': 9, 'z': 3}  — b's y wins
{**a, **b}       # same result (works pre-3.9)
a.update(b)      # mutates a in place

Pick | for a new merged dict, update/|= to modify one in place.

Views: keys, values, items

.keys(), .values(), and .items() return dynamic views, not lists — they reflect later changes to the dict and avoid copying. Wrap in list(...) only if you need a snapshot.

d = {"a": 1, "b": 2}
keys = d.keys()
d["c"] = 3
list(keys)       # ['a', 'b', 'c'] — the view updated live

for k, v in d.items():   # idiomatic iteration
    print(k, v)

Key views even support set operations (d1.keys() & d2.keys() for common keys), which is a neat, lesser-known trick.

Dict comprehensions

Build a dict from an iterable in one expression — readable and fast.

squares = {n: n * n for n in range(5)}        # {0:0, 1:1, 2:4, 3:9, 4:16}
inverted = {v: k for k, v in d.items()}       # swap keys and values

Recap

A dict is a hash table giving average O(1) lookups, which is why keys must be hashable/immutable (tuples, not lists). Since 3.7 dicts preserve insertion order; use get/setdefault to handle missing keys without KeyError, the | operator (or {**a, **b}) to merge with right-hand precedence, and the live views from keys()/values()/items() for iteration. Reach for Counter/defaultdict when counting or grouping. Understand the hash table underneath and every dict behaviour follows.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.