Python Part I · Module 8 · Dictionaries
Safe Lookup: in, .get() and the Missing Key
Real data has holes in it. One log was truncated and has no runtime field; one tool version writes a key the others do not. A script that reads a field directly will stop dead on the first record that lacks it, and a summary covering two hundred tests will report nothing at all because of one. Python gives you two other ways to ask, and the point of this chapter is that they are not simply safer — they make a different claim about your data. Choosing between them is the same judgement Chapter 7.3 asked for between remove and discard, and it hinges on one question: is a missing key a normal situation, or evidence that something is already wrong?
Foundation14 min readPythonPart IFundamentalsDictionariesLookup
Part I · Module 8 · Chapter 8.3 · Safe Lookup: in, .get() and the Missing Key
1. What a Missing Key Does
result = {"test": "axi_smoke", "status": "PASS"}
print(result["runtime_s"])KeyError: 'runtime_s'The error names the key it could not find. That is useful — it is telling you precisely which assumption failed.
And it is worth reading result["runtime_s"] as the claim it actually makes:
An empty dictionary behaves the same way, which matters because an empty one is a normal starting state:
print({}["anything"])KeyError: 'anything'2. Asking Whether the Key Is There
The direct question is the membership operator you have used since Chapter 3.6:
result = {"test": "axi_smoke", "status": "PASS"}
print("status" in result)
print("runtime_s" in result)True
FalseIt produces an ordinary boolean, like every other use of in:
result = {"test": "axi_smoke", "status": "PASS"}
has = "status" in result
print(has, type(has))True <class 'bool'>And not in asks the opposite:
result = {"test": "axi_smoke", "status": "PASS"}
print("runtime_s" not in result)TrueThe part that catches people
Now the trap, and it is a good one:
result = {"test": "axi_smoke", "status": "PASS"}
print("PASS" in result)
print("axi_smoke" in result)False
FalseBoth False — and both of those strings are plainly in the dictionary. They are values, not keys.
x in some_dict asks only one question: is x one of the keys? It does not look at values at all.
This follows from Chapter 8.1's model rather than being an arbitrary rule. A dictionary is organised by key — that is how it finds anything. Asking it about a value would mean searching every entry, which is not what the structure is built to do.
3. .get(): Asking Without Asserting
The second approach is a dictionary method that returns a value if the key is present and does not raise if it is not:
result = {"test": "axi_smoke", "status": "PASS"}
print(result.get("status"))PASSIdentical to result["status"] when the key exists. The difference appears when it does not:
result = {"test": "axi_smoke", "status": "PASS"}
print(result.get("runtime_s"))
print(type(result.get("runtime_s")))None
<class 'NoneType'>None rather than a KeyError. Chapter 2.8 introduced None as there is no value here, and that is exactly what it means in this position.
4. Supplying Your Own Default
None is rarely the most useful answer. A second argument says what to produce instead:
result = {"test": "axi_smoke", "status": "PASS"}
print(result.get("runtime_s", 0.0))
print(result.get("status", "UNKNOWN"))0.0
PASSThe first key is absent, so the default 0.0 comes back — a number, which arithmetic can use immediately. The second key is present, so the default is ignored entirely and the real value is returned.
That is the shape the canonical curriculum names for this chapter: a summary that must keep working when one log is missing a field. Rather than stopping, the missing runtime becomes 0.0 and the report continues.
One thing .get() does not do is add the key:
result = {"test": "axi_smoke", "status": "PASS"}
print(len(result))
result.get("runtime_s", 0.0)
print(len(result))
print("runtime_s" in result)2
2
FalseTwo entries before, two after. .get() only reads — the dictionary is unchanged, and the default was produced for you rather than stored. Adding entries is Chapter 8.4.
5. The Limitation Worth Knowing
.get() cannot always distinguish missing from present but empty:
d2 = {"runtime_s": None}
print(d2.get("runtime_s"))
print(d2.get("missing"))
print(d2.get("runtime_s") == d2.get("missing"))None
None
TrueThe first key exists and is mapped to None. The second does not exist at all. Both calls return None, and the two results compare equal — so on that evidence alone the two situations are indistinguishable.
Membership does tell them apart:
d2 = {"runtime_s": None}
print("runtime_s" in d2)
print("missing" in d2)True
False6. The Default Fires on Absence, Not on Falsiness
This is the most common wrong model of .get(), and it is silent when you have it:
d3 = {"errors": 0}
print(d3.get("errors", 99))
print(d3.get("warnings", 99))0
99The first is 0, not 99. The key "errors" is present, so its value is returned — and that value happens to be zero. The default was never considered.
The second is 99, because "warnings" genuinely is not there.
7. Which One to Use
These are three different claims, not three levels of safety:
| Form | The claim it makes | When the key is absent |
|---|---|---|
d["k"] | this key must exist | KeyError — stops immediately |
"k" in d | I want to know whether it exists | False |
d.get("k", default) | absence is acceptable, use this instead | returns the default |
Note what this chapter has deliberately not done: it has shown you how to ask whether a key is present, but not how to do one thing when it is and another when it is not. Acting differently on a boolean is the if statement, and Module 9 is where that arrives. Everything here works without it — .get() in particular is designed so that you often do not need a branch at all.
8. Asking About Values Instead
For completeness, since Section 2 raised it. If the question really is does this value occur anywhere, there is a way to ask:
result = {"test": "axi_smoke", "status": "PASS"}
print("PASS" in result.values())
print("NOPE" in result.values())True
Falseresult.values() provides the values, and membership against that checks values. Chapter 8.5 covers .values() properly.
Be clear about the difference in cost, though. "k" in d goes more or less straight to the answer, for the reason Chapter 7.2 gave; v in d.values() has to look through the values one after another, because they are not organised for finding. If you find yourself searching values often, the dictionary may be the wrong way round — Chapter 8.8 is about that.
9. Broken on Purpose
One more, because the message is specific:
print({"a": 1}.get("a", 1, 2))TypeError: get expected at most 2 arguments, got 3.get() takes the key and at most one default. A third argument is not a second fallback.
10. Predict, Run, Observe
Predict all eight before reading on. Three are the traps from this chapter.
rec = {"test": "smoke", "errors": 0, "note": None}
print("test" in rec)
print("smoke" in rec)
print("errors" in rec)
print(rec.get("errors", 5))
print(rec.get("warnings", 5))
print(rec.get("note"))
print(rec.get("missing"))
print(len(rec))The verified output:
True
False
True
0
5
None
None
3"smoke" in rec is False — it is a value, not a key. Section 2.
rec.get("errors", 5) is 0, not 5: the key is present and its value is zero. Section 6.
Lines 6 and 7 both print None for completely different reasons — "note" exists and is mapped to None, "missing" does not exist. Section 5, and the reason in is sometimes the question you actually want.
And len(rec) is 3 throughout: nothing in this chapter changed the dictionary.
11. Exercises
Exercise 1 — three ways to ask. For rec = {"test": "smoke", "status": "PASS"}, write: the line that asserts "status" exists and fetches it, the line that reports whether "runtime_s" exists, and the line that fetches "runtime_s" with a default of 0.0.
Exercise 2 — predict. Say True, False or the exact value for each, then verify:
(a) "status" in rec (b) "PASS" in rec (c) "PASS" in rec.values() (d) rec.get("status") (e) rec.get("nope") (f) rec.get("nope", "?")
Exercise 3 — the keys-only rule. Explain in three sentences why "PASS" in rec is False, referring to what a dictionary is organised by, and give the expression that asks the question the author probably meant.
Exercise 4 — the falsy default. Predict both, verify, and explain the difference in two sentences:
d = {"errors": 0, "warnings": 2}
print(d.get("errors", 10))
print(d.get("fatals", 10))Then say what would go wrong in a report that used the first form to avoid zeros.
Exercise 5 — missing or empty. Given d = {"note": None}, predict d.get("note"), d.get("absent") and "note" in d. Explain in two sentences which expression distinguishes the two situations and why .get() alone cannot.
Exercise 6 — choose the form. For each, say which of the three forms you would use and justify in one sentence: (a) reading the status of a record your own parser just built, where every record must have one (b) reading an optional comment field that only some tools emit (c) reporting how many records contain a runtime field (d) reading a count that should default to zero when the field is absent
Exercise 7 — read the error. Run {"a": 1}["b"] and write down the exact message. Say in two sentences what assumption failed and why stopping is more useful here than returning None would be.
Exercise 8 — the cost difference. Explain in two sentences why "k" in d and v in d.values() are not equally cheap, referring to how a dictionary finds things.
12. Summary
d["k"]asserts the key exists. When it does not,KeyError: 'k'names the failed assumption and stops."k" in dasks whether the key is present and produces abool.- Membership checks keys only.
"PASS" in resultisFalseeven when"PASS"is a value — silently, because the boolean is valid. d.get("k")returns the value, orNoneif the key is absent — no exception.d.get("k", default)returns your default instead ofNone..get()never adds the key and never changes the dictionary.- The default fires on absence, not on falsiness.
{"errors": 0}.get("errors", 99)is0. .get()cannot distinguish a missing key from a key mapped toNone— both returnNone. Useinwhen that difference matters.- Choose by what absence means: direct lookup when it signals a bug,
.get()when it is normal variation. Neither is generally safer, and.get()everywhere hides real failures. v in d.values()asks about values, and has to search rather than look up.
Reading a dictionary is settled. The next chapter changes one.
Where this fits
Part of the Python curriculum.
