Python Part I · Module 8 · Dictionaries
Adding, Updating and Removing Entries
A results table starts empty and fills up as work finishes. A record gets a field corrected when a second pass finds better information. Both are changes to a dictionary that already exists, and Python expresses them with a syntax you have already seen used for something else entirely — assigning to a subscript. On a list that required the position to exist; here it does not, and the same line either adds a new entry or overwrites an existing one depending on nothing you can see at the call site. That is convenient and it is worth understanding precisely, because the difference between adding and replacing is the difference between a table of two hundred results and a table of one.
Foundation15 min readPythonPart IFundamentalsDictionariesMutation
Part I · Module 8 · Chapter 8.4 · Adding, Updating and Removing Entries
1. One Syntax, Two Behaviours
Start with an empty dictionary and assign to a key that does not exist:
results = {}
print(len(results))
results["axi_smoke"] = "PASS"
print(results, len(results))0
{'axi_smoke': 'PASS'} 1The entry was added. There was no "axi_smoke" key, and now there is.
Now assign to the same key again:
results = {"axi_smoke": "PASS"}
results["axi_smoke"] = "FAIL"
print(results, len(results)){'axi_smoke': 'FAIL'} 1The value was replaced. Still one entry; the old "PASS" is gone.
Both lines are d[key] = value. Nothing at the call site tells you which will happen — it depends entirely on whether the key is already there.
2. Telling Them Apart
len() is the instrument:
r = {"a": 1}
print(len(r))
r["b"] = 2
print(len(r))
r["b"] = 3
print(len(r))
print(r)1
2
2
{'a': 1, 'b': 3}One, then two — an entry was added. Then two again — the second assignment to "b" replaced its value rather than adding anything. The final dictionary shows 3, the value that won.
This matters more than it first appears. A script building a results table keyed by test name will silently collapse two runs of the same test into one entry, keeping only the last. That is sometimes exactly right and sometimes a bug, and len() against the number of records you processed is how you find out.
3. Growing a Table
The canonical pattern for this chapter: start empty, add as results arrive.
table = {}
table["smoke"] = "PASS"
table["reset"] = "FAIL"
table["crc"] = "PASS"
print(table)
print(len(table)){'smoke': 'PASS', 'reset': 'FAIL', 'crc': 'PASS'}
3Three results accumulated into one structure, each findable by name.
The entries come out in the order they were added — the insertion-order property from Chapter 8.1:
table = {}
table["smoke"] = "PASS"
table["reset"] = "FAIL"
table["crc"] = "PASS"
print(list(table))['smoke', 'reset', 'crc']Not alphabetical. The order you added them.
What is missing from that example is the part that does the repeating — a real script does not know its test names in advance, it works through logs and adds one entry per log. Repeating work is a loop, and Chapter 8.5 covers going through a dictionary while Module 10 covers loops as a capability. The accumulation itself is what this chapter owns, and it does not change when the loop arrives: table = {} before, table[name] = status as you go, len(table) after.
Chapter 5.3 described exactly this shape for lists. The difference is that a list accumulates a sequence of things and a dictionary accumulates an answer per subject — which is why the dictionary version can be asked about one test later.
4. Updating a Record
The other shape from Chapter 8.2 — a record with named fields — uses the same syntax to correct a field:
rec = {"test": "axi_smoke", "status": "PASS", "errors": 0}
rec["status"] = "FAIL"
rec["errors"] = 3
print(rec){'test': 'axi_smoke', 'status': 'FAIL', 'errors': 3}Two fields overwritten in place. Notice what did not happen: the record was not rebuilt. You did not have to write out the unchanged "test" field, and anything else referring to this dictionary sees the corrected version.
Adding a field to a record works identically:
rec = {"test": "axi_smoke", "status": "FAIL", "errors": 3}
rec["runtime_s"] = 18.2
print(rec)
print(len(rec)){'test': 'axi_smoke', 'status': 'FAIL', 'errors': 3, 'runtime_s': 18.2}
4Four fields now. A record's keys are usually fixed by what the record is, so adding one is a decision worth noticing rather than a routine operation — but nothing in Python stops you.
Mutation is not rebinding
The distinction Chapters 5.3 and 5.8 insisted on applies here unchanged:
rec = {"status": "PASS"}
rec["status"] = "FAIL"
print(rec){'status': 'FAIL'}That mutated the dictionary — the same dictionary now maps "status" differently.
rec = {"status": "PASS"}
rec = {"status": "FAIL"}
print(rec){'status': 'FAIL'}That rebound the name — a brand-new dictionary was built and rec was pointed at it. The original was not touched.
Both lines end with rec showing FAIL, and they are different operations. Chapter 8.6 is where that difference becomes visible and important.
5. Removing an Entry
Two ways, differing in exactly one respect.
del removes the entry:
d = {"a": 1, "b": 2}
del d["a"]
print(d, len(d)){'b': 2} 1pop removes it and hands the value back:
d2 = {"a": 1, "b": 2}
taken = d2.pop("a")
print(taken)
print(d2, len(d2))1
{'b': 2} 1Same effect on the dictionary; pop additionally produced 1, the value that was removed.
Both raise when the key is absent:
d = {"a": 1}
del d["zzz"]KeyError: 'zzz'd = {"a": 1}
d.pop("zzz")KeyError: 'zzz'But pop accepts a default, which makes it not raise — the same idea .get() used in Chapter 8.3:
d3 = {"a": 1}
print(d3.pop("zzz", "not there"))
print(d3)not there
{'a': 1}No error, the default came back, and the dictionary is unchanged.
And clear() empties a dictionary in place:
c = {"a": 1, "b": 2}
c.clear()
print(c, len(c)){} 0Still the same dictionary, now with no entries.
6. Merging With update()
update() takes another dictionary and folds it in. Existing keys are replaced; new keys are added:
base = {"sim": "vcs", "timeout": 60}
print(base)
base.update({"timeout": 120, "seed": 4210})
print(base)
print(len(base)){'sim': 'vcs', 'timeout': 60}
{'sim': 'vcs', 'timeout': 120, 'seed': 4210}
3Read the result carefully. "sim" was not mentioned, so it survived untouched. "timeout" was mentioned, so its value went from 60 to 120. "seed" was new, so it was added. Two entries became three.
That is the same add-or-replace rule from Section 1, applied to every key at once.
It also accepts pairs rather than a dictionary:
p = {"a": 1}
p.update([("b", 2), ("c", 3)])
print(p){'a': 1, 'b': 2, 'c': 3}A list of (key, value) tuples — the shape Chapter 6.7 called a list of tuples, here doing real work.
The None trap, again
update mutates and returns nothing useful:
u = {"a": 1}
print(u.update({"b": 2}))
print(u)None
{'a': 1, 'b': 2}Which means the trap from Chapters 5.3 and 7.3 applies here too:
d = {"a": 1}
d = d.update({"b": 2})
print(d)NoneNo error, and d is now None. The merge happened correctly and then the dictionary was thrown away.
7. The Key Rule Still Applies
Assignment creates entries, but it cannot create an entry with an unusable key:
d = {}
d[["a"]] = 1TypeError: unhashable type: 'list'Chapter 8.2's rule, enforced at the moment you assign rather than only inside a literal.
8. What Each Operation Does
A summary of what you have seen, not the lesson:
| Operation | Effect | Returns |
|---|---|---|
d[k] = v | adds if k is new, replaces if it exists | — (a statement) |
del d[k] | removes the entry | — (KeyError if absent) |
d.pop(k) | removes the entry | the removed value (KeyError if absent) |
d.pop(k, default) | removes if present | the value, or the default |
d.update(other) | replaces existing keys, adds new ones | None |
d.clear() | removes everything | None |
pop is the only row whose result is worth assigning.
9. Predict, Run, Observe
Predict every line before reading on. The length changes are the point.
t = {}
t["smoke"] = "PASS"
t["reset"] = "FAIL"
print(len(t))
t["smoke"] = "SKIP"
print(len(t))
print(t["smoke"])
taken = t.pop("reset")
print(taken)
print(len(t))
t.update({"crc": "PASS", "smoke": "PASS"})
print(len(t))
print(t)The verified output:
2
2
SKIP
FAIL
1
2
{'smoke': 'PASS', 'crc': 'PASS'}Two entries after the first two assignments. The third assignment names "smoke" again, so the length stays 2 and the value becomes "SKIP" — add versus replace, Section 1.
pop("reset") returns "FAIL" and leaves one entry.
Then update adds "crc" and replaces "smoke" — one new key, one existing — so the length goes to 2, not 3.
And look at the final order: smoke before crc, even though "smoke" was re-assigned twice and "crc" arrived later. Replacing a value does not move the key — it keeps the position it had when it was first inserted. That is a detail worth knowing before Chapter 8.5 walks a dictionary in order.
10. Exercises
Exercise 1 — build a table. Starting from an empty dictionary, add results for three tests, then correct one of them. Print len() after every statement and say which lines added and which replaced.
Exercise 2 — predict the length. Starting from d = {"a": 1} each time, give len(d) afterwards:
(a) d["b"] = 2 (b) d["a"] = 2 (c) del d["a"] (d) d.pop("a") (e) d.update({"a": 9, "b": 9}) (f) d.clear()
Exercise 3 — add or replace. Explain in three sentences why d[k] = v needs no separate "add" and "update" operations, and why the same line on a list would raise if the position did not exist.
Exercise 4 — del or pop. For each, choose one and justify in one sentence: (a) discarding a cached entry you no longer need (b) taking the next pending job out of a table so you can work on it (c) removing an entry that may or may not be present
Exercise 5 — the merge. Predict base and len(base) after each step, then verify:
base = {"sim": "vcs", "timeout": 60}
base.update({"timeout": 30})
base.update({"debug": True})Say in one sentence which call added and which replaced.
Exercise 6 — the None trap. This prints None and breaks everything after it. Name the three steps that produced it and fix it with the smallest change:
cfg = {"a": 1}
cfg = cfg.update({"b": 2})
print(cfg)Exercise 7 — the silent collapse. A script reads 200 log files and does table[test_name] = status for each. It finishes with len(table) of 173. Explain in two sentences what that number means about the data, and whether it is necessarily a bug.
Exercise 8 — order after replacement. Predict the printed order, verify, and explain in one sentence:
d = {}
d["x"] = 1
d["y"] = 2
d["x"] = 99
print(list(d))11. Summary
d[k] = vadds when the key is new and replaces when it exists. The code is identical either way.- A dictionary has no fixed positions to run out of, which is why assignment can create an entry where a list's cannot.
len()tells you which happened — and comparing it against the number of records processed catches a silently collapsing table.- Start empty, assign as results arrive. The repetition comes from a loop later; the accumulation is this chapter's.
- Updating a record changes a field in place without rebuilding the record.
- Mutation is not rebinding.
rec["status"] = xchanges the dictionary;rec = {...}changes what the name refers to. del d[k]removes an entry;d.pop(k)removes it and returns the value. Both raiseKeyErrorwhen absent;popaccepts a default and then does not.d.update(other)replaces existing keys and adds new ones — the add-or-replace rule applied to every key at once. It also accepts a list of pairs.updateandclearreturnNone.d = d.update(...)silently setsdtoNone.popis the one worth assigning.- Assignment still requires a hashable key.
- Replacing a value does not move the key — it keeps its original insertion position.
Entries can now be added, changed and removed. The next chapter walks through all of them.
Where this fits
Part of the Python curriculum.
