JSON, TOML, and Encoding

Updated

September 8, 2026

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.py

Output:

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.py

Output:

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.py

Output:

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.py

Output:

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.py

Output:

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.load on "rb". CSV with newline="".
  • 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.loads raise on junk. Catch at the edge.

Try this

  1. In ticket_json.py, add "opened": "2026-09-07T14:30:00+00:00" and print it after load.
  2. In desk_toml.py, add shift = "day" under [desk] and print it.
  3. In tickets_csv.py, add a third row and print status counts with a dict.