JSON, TOML, and Encoding
JSON, TOML, and Encoding
The standard library already speaks the three formats a desk service actually ships: JSON for APIs and files, TOML for config you read, CSV for spreadsheets. Encode times as ISO strings. Do not invent a fourth format this week.
Mental model
JSON (json) is nested dicts, lists, strings, numbers, booleans, and null. It has no datetime type. You store isoformat() text.
TOML is the usual pyproject.toml / app-config language. tomllib reads it (load from bytes, loads from str). It does not write. Writing TOML is a third-party job, or you keep the file as a string in tests.
CSV is rows. Open CSV files with newline="" and an encoding. csv.DictWriter / DictReader keep headers honest.
Text files are UTF-8. tomllib.load wants a binary file ("rb").
Worked examples
Case 1: JSON ticket round-trip
Save as ticket_json.py. dumps / loads for strings. Files use write_text with UTF-8.
# ticket_json.py
import json
from pathlib import Path
from tempfile import TemporaryDirectory
def main() -> None:
ticket = {"id": 7, "table": 12, "status": "open"}
with TemporaryDirectory() as raw:
path = Path(raw) / "ticket.json"
path.write_text(json.dumps(ticket, indent=2) + "\n", encoding="utf-8")
loaded = json.loads(path.read_text(encoding="utf-8"))
print(loaded["id"])
print(loaded["status"])
if __name__ == "__main__":
main()Run:
uv run python ticket_json.pyOutput:
7
open
Case 2: Read TOML config
Save as desk_toml.py. Write the file as bytes (or UTF-8 text), open "rb", tomllib.load.
# desk_toml.py
import tomllib
from pathlib import Path
from tempfile import TemporaryDirectory
CONFIG = """
[desk]
name = "front"
tables = [3, 11, 12]
"""
def main() -> None:
with TemporaryDirectory() as raw:
path = Path(raw) / "desk.toml"
path.write_bytes(CONFIG.encode("utf-8"))
with path.open("rb") as fh:
data = tomllib.load(fh)
print(data["desk"]["name"])
print(data["desk"]["tables"])
if __name__ == "__main__":
main()Run:
uv run python desk_toml.pyOutput:
front
[3, 11, 12]
tomllib.loads(CONFIG) is the same dict if you already have a string.
Case 3: CSV tickets
Save as tickets_csv.py. newline="" is required so the csv module owns line endings.
# tickets_csv.py
import csv
from pathlib import Path
from tempfile import TemporaryDirectory
def main() -> None:
with TemporaryDirectory() as raw:
path = Path(raw) / "tickets.csv"
with path.open("w", encoding="utf-8", newline="") as fh:
writer = csv.DictWriter(fh, fieldnames=["id", "table", "status"])
writer.writeheader()
writer.writerow({"id": 7, "table": 12, "status": "open"})
writer.writerow({"id": 8, "table": 3, "status": "paid"})
with path.open(encoding="utf-8", newline="") as fh:
for row in csv.DictReader(fh):
print(f"ticket {row['id']} table {row['table']} {row['status']}")
if __name__ == "__main__":
main()Run:
uv run python tickets_csv.pyOutput:
ticket 7 table 12 open
ticket 8 table 3 paid
Case 4: tomllib does not dump
Save as toml_write.py. The stdlib reader has no dump. Check, then keep writing JSON or a hand-made TOML string.
# toml_write.py
import tomllib
def main() -> None:
print(hasattr(tomllib, "dump"))
print(hasattr(tomllib, "load"))
if __name__ == "__main__":
main()Run:
uv run python toml_write.pyOutput:
False
True
The trap
json.dumps will not serialize a datetime. Encode it yourself.
Save as json_datetime.py:
# json_datetime.py
import json
from datetime import datetime, timezone
def main() -> None:
opened = datetime(2026, 9, 7, 14, 30, tzinfo=timezone.utc)
try:
json.dumps({"opened": opened})
except TypeError as exc:
print(type(exc).__name__)
print(exc)
print(json.dumps({"opened": opened.isoformat()}))
if __name__ == "__main__":
main()Run:
uv run python json_datetime.pyOutput:
TypeError
Object of type datetime is not JSON serializable
{"opened": "2026-09-07T14:30:00+00:00"}
The second line is the boring encoding. Parse it back with datetime.fromisoformat.
The boring rule
- JSON for data you exchange. TOML for human-edited config you read. CSV for tables other people open in a spreadsheet.
- UTF-8 everywhere.
tomllib.loadon"rb". CSV withnewline="". - Dates go to JSON as ISO-8601 strings with an offset.
- Do not add a TOML writer until you actually generate config files.
json.loads/tomllib.loadsraise on junk. Catch at the edge.
Try this
- In
ticket_json.py, add"opened": "2026-09-07T14:30:00+00:00"and print it after load. - In
desk_toml.py, addshift = "day"under[desk]and print it. - In
tickets_csv.py, add a third row and printstatuscounts with a dict.