PYTHON / DICTIONARIES AND SETS
Dictionary methods and view objects
Use get, setdefault, pop, popitem and update correctly, and work with keys(), values() and items() as live set-like views.
What you will learn
- Pick between get, setdefault and pop based on whether the dict should change
- Treat keys() and items() as live views, not snapshots or lists
- Combine two dicts' keys with &, -, ^ instead of manual loops
- Wrap a view in list() before deleting keys while looping
Understanding Dictionary methods and view objects
The dictionary methods split into three jobs. get and setdefault are lookups that tolerate a missing key: get returns a default and leaves the dict untouched, while setdefault inserts the default and returns it. pop, popitem and clear remove things, and update writes many keys at once from another mapping, an iterable of pairs, or keyword arguments. Knowing which group a method belongs to tells you immediately whether calling it mutates the dictionary.
keys(), values() and items() do not build lists. Each returns a small view object that holds a reference to the dictionary and reads it on demand, so a view created before an insertion still reflects the dictionary after it. That is why len(view) is cheap and why a view is not subscriptable: there is no stored sequence to index into. It is also why changing the dictionary's size during iteration over a view raises RuntimeError instead of silently skipping entries.
Because dictionary keys are unique and hashable, a keys view already satisfies the contract of a set, so Python lets you use &, |, - and ^ directly on it. An items view is set-like too, but only as far as its values allow: the pairs must be hashable for a set operation to succeed. A values view gets none of this, since values may repeat and may be unhashable, so it supports only iteration, len and the in operator.
inventory = {"bolts": 250, "nuts": 400}
keys = inventory.keys()
items = inventory.items()
print(keys, items)
inventory["washers"] = 120
print(keys)
print(len(items))
removed = inventory.pop("nuts")
print(removed, keys)
print(inventory.get("screws"), inventory.get("screws", 0))
print(inventory.setdefault("screws", 0))
print(inventory)keys(), values() and items() are live views onto the dictionary rather than copies, and keys/items views additionally behave like sets.
Worked examples
Set algebra on keys and items
Compares two dictionaries using view operators instead of writing comparison loops.
monday = {"alice": 8, "bob": 6, "cara": 7}
tuesday = {"bob": 6, "cara": 5, "dan": 9}
print(sorted(monday.keys() & tuesday.keys()))
print(sorted(monday.keys() - tuesday.keys()))
print(sorted(monday.keys() ^ tuesday.keys()))
print(sorted(monday.items() & tuesday.items()))
try:
monday.values() & tuesday.values()
except TypeError as e:
print("TypeError:", e)Example explained
Line 1keys() & keys() gives names present in both dicts; the result is a real set, so sorted() makes the printed order deterministic.
Line 2keys() - keys() gives names only in monday, and ^ gives names in exactly one of the two.
Line 3items() & items() matches on key and value together, so only ('bob', 6) survives while cara's differing hours drop out.
Line 4values() has no set operators because values can repeat and can be unhashable, so the & raises TypeError.
Deleting while iterating a view
Shows why a live view forbids resizing mid-loop and how a list() copy fixes it.
counts = {"a": 1, "b": 0, "c": 2, "d": 0}
try:
for key in counts.keys():
if counts[key] == 0:
del counts[key]
except RuntimeError as e:
print("RuntimeError:", e)
print(counts)
for key in list(counts.keys()):
if counts[key] == 0:
del counts[key]
print(counts)Example explained
Line 1The first loop deletes 'b' successfully, then the iterator notices the size changed and raises on the next step.
Line 2The dict is left half-processed: 'b' is gone but 'd' was never reached, which is exactly the silent-corruption case the error prevents.
Line 3list(counts.keys()) materialises the keys once, so the loop iterates over a snapshot that deletion cannot disturb.
update, pop and popitem
Merges one dict into another and removes entries with and without a fallback.
config = {"host": "localhost", "port": 8080}
overrides = {"port": 9090, "debug": True}
config.update(overrides)
print(config)
config.update(timeout=30)
print(config)
print(config.pop("debug"))
print(config.pop("missing", "absent"))
print(config.popitem())
print(config)Example explained
Line 1update overwrites 'port' in place and appends 'debug'; it mutates config and returns None, so never assign its result.
Line 2update also accepts keyword arguments, which is convenient but limits keys to valid identifiers.
Line 3pop returns the removed value, and a second argument turns a missing key into that fallback instead of a KeyError.
Line 4popitem removes the most recently inserted pair, 'timeout', and returns it as a (key, value) tuple.
setdefault versus get
Demonstrates that setdefault writes to the dict and always evaluates its default argument.
def make_list():
print("make_list called")
return []
groups = {"even": [2]}
groups.setdefault("even", make_list()).append(4)
groups.setdefault("odd", make_list()).append(3)
print(groups)
print(groups.get("prime", []))
print("prime" in groups)Example explained
Line 1make_list runs twice, including for the existing 'even' key, because Python evaluates arguments before setdefault sees them.
Line 2For 'even' the freshly built list is discarded and the stored list [2] is returned, so append(4) lands in the dict.
Line 3For 'odd' the default is inserted and returned, so append(3) mutates the value now living in groups.
Line 4get returns its default without inserting anything, which is why 'prime' in groups is still False.
Important notes
setdefault always evaluates its second argument, so avoid it when the default comes from an expensive call or a function with side effects.
Equality on views follows set semantics for keys() and items(), but two dict_values objects never compare equal unless they are the same object.
Common mistakes
Indexing a view, as in d.keys()[0], which raises TypeError: 'dict_keys' object is not subscriptable because no list is ever built.
Assuming get(key, []) registers the key, then wondering why appending to the returned list has no effect on the dictionary.
Deleting keys while looping over d.keys() or d.items(), which raises RuntimeError partway through and leaves the dict half-cleaned.
Writing d = d.update(other) and ending up with None, since update mutates in place and returns nothing.
Try it yourself
Change, predict, then run
Given old = {'a': 1, 'b': 2, 'c': 3} and new = {'b': 2, 'c': 30, 'd': 4}, use view operations to print the added keys, the removed keys, and the keys whose value changed, then remove every changed key from old with pop.
Open the Python workspaceCheck your understanding
Why does d1.items() & d2.items() usually work while d1.values() & d2.values() raises TypeError?
- Values may repeat and may be unhashable, so a values view cannot honour set semantics
- values() returns a plain list, and lists have no set operators
- Set operators only work between views taken from the same dictionary
- items() is set-like because tuples are always hashable, unlike arbitrary values
Show answer
A set requires unique, hashable members. Keys guarantee both, so keys and items views expose set operators, while values guarantee neither. Option 4 is tempting but wrong on two counts: a tuple is hashable only if its contents are, so d1.items() & d2.items() also fails when values are unhashable, such as lists.