JSON & Files
Agent work is mostly moving structured data around. JSON is the format that data arrives in, and files are where it rests. Neither is complicated; both have a few traps worth meeting deliberately.
JSON is a dictionary that has been turned into text
{"role": "user", "content": "Hello", "tokens": 12}That is text. To work with it in Python you turn it into a dictionary, and to send it anywhere you turn it back.
Two function names carry the whole library:
import json
text = '{"role": "user", "content": "Hello"}'
data = json.loads(text)
print(data["role"])
back = json.dumps(data)
print(back)loads— load string: text becomes a Python objectdumps— dump string: Python object becomes text
The trailing s means "string". Without it, json.load and json.dump work on an open file rather than a string. That single letter is the most common mix-up on this page.
How types map
| JSON | Python |
|---|---|
| object | dict |
| array | list |
| string | str |
| number | int or float |
true / false | True / False |
null | None |
The capitalisation of true and null is the visible difference. If you hand-write JSON inside a Python file and use True, it will not parse.
Reading nested data
Real responses nest, and this is where people get stuck.
response = {
"status": "success",
"ticket": {"id": 4471, "severity": "P2", "tags": ["network", "urgent"]},
}
print(response["ticket"]["severity"])
print(response["ticket"]["tags"][0])Work left to right, one step at a time. response["ticket"] is a dictionary, so you can index it again. ["tags"] is a list, so you index it by position.
When you are unsure of the shape, print it:
print(json.dumps(response, indent=2))indent=2 prints it laid out over multiple lines. This one line will save you more time than anything else on this page.
Missing keys
response["ticket"]["owner"]KeyError: 'owner'Square brackets demand the key exists. When it might not, use .get():
owner = response["ticket"].get("owner", "unassigned")For anything that came from a model or an external API, assume keys may be missing. This is the single most common crash in agent code, and .get() with a sensible default removes most of it.
Reading and writing files
Always use with. It closes the file for you, even if something fails halfway.
with open("notes.txt", "r", encoding="utf-8") as f:
text = f.read()
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("first line\n")The mode is the second argument:
| Mode | Meaning |
|---|---|
"r" | read, fails if missing |
"w" | write, wipes the file first |
"a" | append to the end |
"x" | create, fails if it exists |
"w" deletes the existing contents the moment the file is opened. If you meant to add to a log, you want "a".
Always pass encoding
open("notes.txt", encoding="utf-8")Without it, Python uses the machine's default, which differs between Windows and Mac. A file written on one and read on the other can raise UnicodeDecodeError, and any Indian-language text or an emoji in a prompt will trigger it. Pass encoding="utf-8" every time and the problem never appears.
Reading a large file
with open("huge.log", encoding="utf-8") as f:
for line in f:
if "ERROR" in line:
print(line.strip())Looping over the file reads one line at a time. f.read() loads the whole thing into memory and will fall over on a large log.
JSON files
with open("config.json", encoding="utf-8") as f:
config = json.load(f)
with open("results.json", "w", encoding="utf-8") as f:
json.dump(results, f, indent=2)Here the names have no s, because they take a file rather than a string.
indent=2 when writing makes the file readable and diff-friendly in git. Worth it for anything a human will open.
JSONL, and why agent work uses it
A .jsonl file holds one JSON object per line. Golden datasets, eval results and trace logs almost always use it.
with open("dataset.jsonl", "w", encoding="utf-8") as f:
for row in rows:
f.write(json.dumps(row) + "\n")
with open("dataset.jsonl", encoding="utf-8") as f:
rows = [json.loads(line) for line in f]The advantage over one big JSON array: you can append a record without rewriting the file, and you can read it line by line however large it grows.
When the model returns almost-JSON
A model asked for JSON will sometimes wrap it in a code fence:
{"severity": "P1"}
json.loads on that fails. You can strip the fence:
cleaned = text.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip()
data = json.loads(cleaned)But the better answer is not to be in this position. Use structured output with a Pydantic model and the framework guarantees the shape. Reach for string-cleaning only when you have no choice — it is the fragile path, and it is where a lot of flaky agent code comes from.
Paths
from pathlib import Path
data_dir = Path("data")
file = data_dir / "config.json"
print(file.exists())
text = file.read_text(encoding="utf-8")pathlib builds paths with / and works on Windows and Mac alike. It saves you from "data" + "\\" + "config.json" and the bugs that come with it.
Coming from Java or C#
| Task | Java | C# | Python |
|---|---|---|---|
| Parse JSON | Jackson readValue | JsonSerializer.Deserialize | json.loads |
| Write JSON | writeValueAsString | Serialize | json.dumps |
| Read a file | Files.readString | File.ReadAllText | open(...).read() |
| Auto-close | try-with-resources | using | with |
| Paths | Path.of | Path.Combine | pathlib.Path |
One real difference: Jackson and System.Text.Json map straight onto a typed class. Python's json gives you a plain dictionary with no checking at all. If you want the typed-object experience, that is what Pydantic is for — parse with json.loads, then validate with a model.
Common mistakes
loadvsloads. Thesmeans string.- Opening with
"w"when you meant"a", and wiping the file. - No
encoding="utf-8", then aUnicodeDecodeErroron someone else's machine. data["key"]on data you did not create. Use.get().f.read()on a huge file.- Hand-parsing model output instead of using structured output.
Practise this
- Turn a dictionary into JSON text and back again, and print it with
indent=2. - Read a nested response and pull out a value three levels deep.
- Write five records to a
.jsonlfile, then read them back into a list. - Trigger a
KeyError, then fix it twice — once with.get()and once with anif.