JJoeven

Curriculum/Python

JSON and Text

Round-trip Python data with dumps and loads. Know what JSON allows, fake a file with a string, and split a CSV line.

beginner21 min20 / 37

Agents live on two kinds of text: prose (prompts and logs) and JSON (tool calls, API bodies, saved state).

JSON is a text format for data. It looks like Python dicts and lists, with small differences. Python’s json module is stdlib. You will use it every day. This site may not have a real disk. We simulate files with strings. Simulate means “pretend.” A string can hold the same text a file would hold.

If you cannot round-trip a dict through JSON, you cannot save memory, cannot parse a tool call, and cannot talk to most APIs. This lesson is that round trip.

Dump, then load
dictdumpsJSON textloads

dumps turns data into text. loads turns text back into data. Never eval model text.

Dump, then load

What you will learn

  • json.dumps and json.loads
  • indent and sort_keys
  • What JSON allows
  • Strings as fake files, and utf-8 in one sentence
  • A tiny CSV line with split

dumps and loads

  • json.dumps(obj) — Python → JSON string. The “s” means string.
  • json.loads(text) — JSON string → Python.

On a real computer, dump / load (no “s”) talk to file objects. Here we stay with strings. Remember the s: string. Without the s: file. Mixing them up is a TypeError.

python
import json
state = {class="tok-s">"goal": class="tok-s">"fix the test", class="tok-s">"steps": 2}
text = json.dumps(state)
print(text)
print(json.loads(text)[class="tok-s">"goal"])

Never use eval on model text. eval runs code. json.loads only reads data. A later lesson repeats this because people forget when a model wraps JSON in words.

dumps fails if the object holds a set, a datetime, or a custom class. Convert those first: list(a_set), or a string timestamp. None becomes null. Tuples become JSON arrays (lists when loaded back). You lose “this was a tuple.”

CallDirectionInputOutput
json.dumps(obj)Python to textdict / list / …str
json.loads(text)text to PythonJSON strdict / list / …
json.dump(obj, file)Python to filefile objectNone (writes)
json.load(file)file to Pythonfile objectdict / list / …

json.load(text) when text is a string fails: a string has no .read the way a file does. Use loads. The extra s is the whole difference.

Pretty print

  • indent=2 adds spaces so people can read the text.
  • sort_keys=True sorts keys A–Z. That makes two dumps easier to compare.
python
import json
print(json.dumps({class="tok-s">"b": 2, class="tok-s">"a": 1}, indent=2, sort_keys=True))

Pretty JSON is for logs and files humans read. Compact JSON (no indent) is smaller for prompts. Same data. Different whitespace. loads accepts both.

sort_keys helps tests: two dicts with the same fields dump to the same text. Without sort, key order can differ and a string compare fails even when the data matches. For equality in tests, compare the loaded dicts, not always the text.

What JSON allows

JSONPython
object {...}dict
array [...]list
stringstr
numberint or float
true / falseTrue / False
nullNone

JSON does not allow comments, trailing commas, or Python sets. It does not allow single quotes. {'q': 'rain'} is Python, not JSON. "q": "rain" inside braces with double quotes is JSON.

Models often add trailing commas or single quotes. Then json.loads raises json.JSONDecodeError. Catch it and retry. Do not eval. Do not replace("'", '"') as a general parser. That breaks apostrophes inside strings.

True/false/null in JSON are lowercase. Python’s True dumps as true. After loads, you have Python True again.

Round-trip surprises

Round-trip a value whenever you are unsure: dumps then loads then ==.

Python inJSON textPython out
True / Falsetrue / falseTrue / False
NonenullNone
(1, 2)[1, 2][1, 2] (a list)
{1, 2}error— convert with list first
{"n": 1}objectsame dict

If your test compares a tuple to the loaded value, it will fail even though the data is “the same.” Dump tuples as lists on purpose, or accept lists after load.

json.dumps(x, ensure_ascii=True) is the default and escapes non-ASCII. For logs you may want ensure_ascii=False so a city name stays readable. For APIs, follow the server.

Fake files, utf-8, and a tiny CSV

utf-8 is a way to store letters as bytes. English, accents, and other alphabets all fit. On a real machine, open text files with encoding="utf-8".

Here, a string is the file:

python
saved = json.dumps(state, indent=2)
loaded = json.loads(saved)

CSV is text with commas. One row can be split on ",". This is enough for a tiny table. It is not a full CSV parser. Fields that contain commas need a real library later. For atlas,paris,ok, split is fine.

python
line = class="tok-s">"atlas,paris,ok"
name, city, status = line.split(class="tok-s">",")
print(name, city, status)

Do not build JSON by joining strings if values can hold quotes. dumps escapes those characters for you. That is the same warning as the strings lesson, now with the right tool.

JSONL means one JSON object per line. A cheap eval file is dumps(case) for each case, joined with newlines. Reading it is for line in text.splitlines(): json.loads(line). Nested args survive. CSV does not like nested args. Prefer JSONL when a column would have to hold a dict.

Walkthrough: the errors models actually send

python
import json

def try_load(label, text):
    try:
        print(label, json.loads(text))
    except json.JSONDecodeError as e:
        print(label, class="tok-s">"decode error")

try_load(class="tok-s">"ok", class="tok-s">'{"q": "rain"}')
try_load(class="tok-s">"single quotes", class="tok-s">"{'q': 'rain'}")
try_load(class="tok-s">"trailing comma", class="tok-s">'{"q": "rain",}')
try_load(class="tok-s">"python True", class="tok-s">'{"ok": True}')

Single quotes fail. A trailing comma fails. Python’s True inside a string that you thought was JSON fails — JSON wants true. The model wrapping JSON in “Sure! {...} thanks” also fails until you slice out the object. Catch JSONDecodeError. Return {"ok": False, "error": "bad json"}. Do not crash the loop.

What goes wrong

  • eval on model output.
  • json.load on a string (needs a file object).
  • Single quotes, trailing commas, Python True / None in the text.
  • Forgetting that null becomes None.
  • Dumping a set.
  • Splitting CSV that contains commas inside fields.
  • Building JSON with string concat so a quote in the query breaks the object.
  • Comparing dumped text in tests without sort_keys when you meant “same data.”
Live PythonOpen full playgroundpython
Output
Run to execute this in your browser. Nothing is sent to a server.

No real file is opened. The saved text is a string. Change error to a string message, dump, load, and confirm it is not None. The tuple line should print a list [1, 2].

How agents use this

A model action is often a JSON object. Your runtime loads it, runs a tool, then dumps the result into the next prompt. Saved memory is JSON text. A string is enough to practice: dump, store, load. On a real disk, write that same string with utf-8. Fail here and you cannot even read what the model asked for.

Pretty dumps belong in traces you read. Compact dumps belong in prompts you pay for. sort_keys helps tests. For equality, compare the loaded dicts when whitespace should not matter.

CSV shows up as cheap eval sets: one row per example, columns for input and expected tool. split is a start. JSONL (one JSON object per line) is often better for nested args. You already have dumps and splitlines. That file format is those two functions.

Never hand the model a Python dict printed with print(state). print uses single quotes and None. That is not JSON. The next loads will fail. Always dumps when the next reader is a JSON parser — including the model, if you asked it to read an object.

When parse fails, keep the raw string in the trace (shortened if huge). “bad json” plus a 200-character prefix is enough to see a trailing comma. Without the raw text, you will guess. Guessing at this layer wastes a whole debug session.

Check your understanding

Which call turns a Python dict into a JSON string?