Dictionaries
A dict maps a key to a value. Learn [], .get, in, items, nested args, and why this is how JSON and tool calls look.
A dict (dictionary) maps a key to a value. A key is the name you look up. A value is the data stored under that name. Keys are usually strings. Values can be anything: numbers, text, lists, other dicts, None.
This is how tool calls look: {"name": "search", "args": {"q": "..."}}.
Look up the key. You get the value. Tool calls are dicts: name, args, result.
A key points at a valueJSON is a text format for objects. Models send JSON. In Python, that object becomes a dict. If lists are memory, dicts are records with named fields. Named fields beat guessing that index 2 is the query.
Keys in one dict are unique. Writing d["name"] = "read" when name already exists replaces the value. It does not add a second name key.
Keys and values
Look up with square brackets. Assign to add or replace a field.
action = {class="tok-s">"type": class="tok-s">"tool", class="tok-s">"name": class="tok-s">"search"}
print(action[class="tok-s">"name"])
action[class="tok-s">"name"] = class="tok-s">"read"
action[class="tok-s">"ok"] = True
print(action)action["thought"] crashes if thought is missing. That error is called KeyError: the key is not in the dict. When a missing key is normal, use .get. When a key must exist, square brackets are fine. A crash then means your parser failed, which is information.
Keys can be strings, numbers, or tuples of immutable values. Beginners should use strings. 0 as a key is legal and confusing. "0" is a different key from 0.
.get, in, keys, values, items
in on a dict checks keys, not values. "name" in action is True. "search" in action is False unless you have a key called "search". To ask “is this value present?”, walk .values() or keep a list. Do not write "search" in action expecting to find the tool name in the values.
| Code | What you get |
|---|---|
d[k] | The value, or a crash if k is missing |
d.get(k) | The value, or None |
d.get(k, default) | The value, or your default |
k in d | True if that key exists |
d.keys() | The keys |
d.values() | The values |
d.items() | Each (key, value) pair |
d.pop(k) | Remove a key and return its value |
Walk both names and values with .items():
for key, value in action.items():
print(key, value)list(d.keys()) is a list of key names, useful to print. Direct print(d.keys()) shows a dict_keys view. Wrap with list when you want a normal list.
.get("thought", "") is a good default for optional text. Then a missing thought is "", not None. Empty text is easier to print and to join. Use .get("count", 0) for optional numbers. Use .get("args", {}) for an optional inner dict. Match the default to the type you want next.
Nesting and JSON-shaped data
Nesting means a value is another dict (or a list). JSON is nested dicts and lists. Walk one level at a time. Print the middle value if you get lost.
d.get(k, default) is the safe lookup. A useful default for a missing inner dict is {}:
q = action.get(class="tok-s">"args", {}).get(class="tok-s">"q")
print(q)If args is missing, the first .get gives {}. The second .get gives None instead of crashing. If args is present but q is missing, you also get None. If you need a string, pass a second default on the inner get: .get("q", "").
Updating nested data changes the inner object:
action[class="tok-s">"args"][class="tok-s">"k"] = 5That requires args to exist and to be a dict. If you are not sure, set a whole inner dict: action["args"] = {"q": "python lists", "k": 5}.
To drop a field, d.pop("thought", None) removes the key if it exists and does not crash if it does not. Use that when you compact a row before sending it back to a model: huge HTML in result can cost tokens. Keep ok and a short result. The original dict can stay in your log file.
Copying a dict with dict(d) or {**d} is shallow. The inner args dict is still shared. If you compact by mutating args, you also change the transcript’s copy. Build a new inner dict when you isolate: {"name": d["name"], "args": dict(d.get("args", {}))}. Named fields make that copy obvious. Index-based records do not.
Walk keys you expect, not every key a model invented. Extra keys are allowed in JSON. Your code should .get the ones you need and ignore the rest, unless you are writing a strict schema check. Strict is good at the tool edge. Loose is fine inside a trace viewer.
Common mistakes
d["thought"]when the key is optional —KeyError."search" in actionwhen you meant the value, not the key.- Defaulting a missing dict to
Nonethen calling.getonNone. - Using a list as a record (
[name, args]) instead of a dict when fields have names. - Mutating a nested dict that is shared with another record (copy lesson later).
- Forgetting that
0and"0"are different keys.
Run to execute this in your browser. Nothing is sent to a server.
Print action.get("args", {}).get("q") after deleting the idea of args from your head: add a second dict with no args key and use the same get chain. You should see None, not a crash.
How agents use this
The model sends an object: tool name, args, result. You store that as a dict. Read name, then pass args into a function. Use .get so a missing key does not crash the loop. The observation you append is another dict. Learn to read a dict like a form with named fields.
A stable shape helps tests: always {"ok": True, "result": ...} or {"ok": False, "error": ...}. Do not return a string on success and a dict on failure. Then every caller needs two readers.
Trace rows are dicts: {"role": "user", "content": "..."}. Tool calls are dicts. HTTP JSON is dicts. Once you can .get and nest, you can walk almost every payload an agent sees. The next lesson is only “more of this,” not a new idea.
Check your understanding