PYTHON / DICTIONARIES AND SETS
Reading, adding, and updating keys
Read dictionary values safely with [], get, and in, and add or replace keys with assignment, setdefault, update, and |.
What you will learn
- Read with d[key] when the key must exist, d.get(key, fallback) when it may not
- Use d[key] = value for both inserting a new key and replacing an existing one
- Use setdefault to insert-and-return a default so you can mutate it in place
- Merge many keys at once with update() (in place) or | (new dict)
Understanding Reading, adding, and updating keys
A dictionary maps each key to one slot holding a value. Reading with d[key] only looks at slots that already exist, so a missing key raises KeyError rather than inventing a value. Assignment is a different operation: d[key] = value tells the dictionary to store something at that key, so it creates the slot when the key is absent and overwrites the old value when it is present. That is why there is no separate add method and no insert-versus-replace distinction to remember.
When you are unsure a key exists, you have three tools with different costs. The `in` operator answers only the existence question. d.get(key) returns None instead of raising, and d.get(key, fallback) returns your own stand-in value, but neither writes anything back into the dictionary. d.setdefault(key, default) is the one that does write: it inserts default if the key is missing, and in every case returns the value now stored under that key.
The distinction matters most when values are mutable. d.get("tags", []).append("x") appends to a throwaway list that nobody kept a reference to, so the dictionary is unchanged. d.setdefault("tags", []).append("x") returns the list that is actually inside the dictionary, so the append is visible afterwards. Note also that the default argument is evaluated before setdefault runs, so an expensive or side-effecting default is computed even when the key already exists.
stock = {"apples": 12, "pears": 4}
print(stock["apples"])
print(stock.get("plums"))
print(stock.get("plums", 0))
stock["plums"] = 7 # key absent, so it is added
stock["apples"] = 20 # key present, so the value is replaced
stock["pears"] += 2 # read, add, store back
print(stock)
print("figs" in stock)
try:
stock["figs"]
except KeyError as e:
print("KeyError:", e)Reading a key requires it to already exist, while assigning to a key creates it or replaces its value with the same syntax.
Worked examples
setdefault versus get for mutable values
Shows that setdefault stores and returns the real value while get only hands back a fallback.
scores = {}
scores.setdefault("ana", []).append(9)
scores.setdefault("ana", []).append(7)
scores.setdefault("bo", []).append(5)
print(scores)
print(scores.setdefault("ana", []) is scores["ana"])
print(scores.get("cy", []))
print(scores)Example explained
Line 1The first setdefault finds no 'ana', stores the new empty list, and returns it, so append lands inside the dictionary.
Line 2The second call sees 'ana' already present, discards the freshly built [], and returns the stored list, giving [9, 7].
Line 3The `is` check confirms setdefault handed back the same object the dictionary holds, not a copy.
Line 4get("cy", []) returns the fallback list without inserting anything, so 'cy' never appears in scores.
Updating many keys at once
Compares update(), which mutates in place, with |, which builds a new dictionary.
defaults = {"host": "localhost", "port": 8000, "debug": False}
user = {"port": 9000, "debug": True}
config = defaults.copy()
config.update(user)
print(config)
config.update(debug=False, timeout=30)
print(config)
merged = defaults | {"port": 1234}
print(merged)
print(defaults["port"])Example explained
Line 1update(user) overwrites 'port' and 'debug' in place and would append any key that config lacked.
Line 2The keyword form update(debug=False, timeout=30) works because those key names are valid identifiers; 'timeout' is new so it goes on the end.
Line 3defaults | {...} returns a brand new dictionary, keeping the left operand's key order with the right operand's values winning.
Line 4defaults["port"] is still 8000, proving | did not touch the original.
Important notes
The default in d.setdefault(key, expr) is evaluated every call, even when the key is present, so avoid expensive or side-effecting expressions there.
The | and |= dictionary merge operators require Python 3.9 or newer; update() works on every version.
Common mistakes
Writing total += d.get("count") for a missing key: get returns None and the addition raises TypeError, so pass an explicit default like d.get("count", 0).
Doing d.get("items", []).append(x) and then wondering why d is empty; the fallback list is discarded because get never stores it.
Treating d.get(key) returning None as proof the key is absent, when the key may genuinely hold None; check `key in d` instead.
Expecting d["new"] = 1 to fail because the key does not exist, then being surprised that a typo in a key name silently creates a second entry.
Try it yourself
Change, predict, then run
Start from prices = {"tea": 3, "coffee": 5} and write code that raises the coffee price by 2, adds "cocoa" at 4, prints the price of "juice" as 0 without inserting it, and finally prints the dictionary to confirm "juice" is absent.
Open the Python workspaceCheck your understanding
A dictionary d holds {"a": 1}. Reading d["b"] raises KeyError, yet d["b"] = 2 works immediately afterwards. Why?
- Subscript assignment is a request to store a value, so the dictionary creates the slot; subscript reading can only return a slot that already exists.
- Dictionaries create keys on read as well, but only after the first successful write to the dictionary.
- Assignment works because dictionaries pre-allocate slots for short string keys, while reads check only filled slots.
- Square brackets are write-only on dictionaries; reads must go through .get(), which is why d["b"] failed.
Show answer
Insertion and replacement are the same store operation, so assigning to any hashable key succeeds whether or not it was present, while a read has nothing to return for an absent key. The last option is tempting because .get() would indeed have avoided the error, but square brackets read perfectly well; they simply raise KeyError when the key is missing.