|$ curl https://forge-ai.dev/api/markdown?path=docs/python/json-csv
$cat docs/json-&-csv.md
updated Today·18-22 min read·published
JSON & CSV
Introduction
JSON is the lingua franca of APIs; CSV is the lingua franca of spreadsheets. The stdlib json and csv modules cover both without third-party deps.
json dumps / loads
json_basics.py
Python
| 1 | import json |
| 2 | from pathlib import Path |
| 3 | |
| 4 | obj = {"name": "Ada", "ok": True, "n": 42, "tags": ["math", "code"]} |
| 5 | s = json.dumps(obj, indent=2, sort_keys=True, ensure_ascii=False) |
| 6 | print(s) |
| 7 | data = json.loads(s) |
| 8 | assert data["name"] == "Ada" |
| 9 | |
| 10 | path = Path("data.json") |
| 11 | path.write_text(json.dumps(obj), encoding="utf-8") |
| 12 | loaded = json.loads(path.read_text(encoding="utf-8")) |
| 13 | |
| 14 | # dump/load with file objects |
| 15 | with path.open("w", encoding="utf-8") as f: |
| 16 | json.dump(obj, f, indent=2) |
| Parameter | Effect |
|---|---|
| indent=2 | Pretty-print |
| sort_keys=True | Stable key order |
| ensure_ascii=False | Keep Unicode as-is |
| separators=(',', ':') | Compact output |
| default=fn | Hook for non-JSON types |
| parse_float=Decimal | Exact decimals |
Encoders, Decoders & Dataclass Hooks
json_hooks.py
Python
| 1 | import json |
| 2 | from dataclasses import dataclass, asdict, is_dataclass |
| 3 | from datetime import datetime, timezone |
| 4 | from enum import Enum |
| 5 | from pathlib import Path |
| 6 | from typing import Any |
| 7 | |
| 8 | class Role(Enum): |
| 9 | ADMIN = "admin" |
| 10 | USER = "user" |
| 11 | |
| 12 | @dataclass |
| 13 | class User: |
| 14 | id: int |
| 15 | name: str |
| 16 | role: Role |
| 17 | created: datetime |
| 18 | |
| 19 | def to_jsonable(o: Any) -> Any: |
| 20 | if is_dataclass(o) and not isinstance(o, type): |
| 21 | return {**asdict(o), "role": o.role.value, "created": o.created.isoformat()} |
| 22 | if isinstance(o, datetime): |
| 23 | return o.isoformat() |
| 24 | if isinstance(o, Enum): |
| 25 | return o.value |
| 26 | if isinstance(o, Path): |
| 27 | return str(o) |
| 28 | raise TypeError(f"not JSON serializable: {type(o)}") |
| 29 | |
| 30 | u = User(1, "Ada", Role.ADMIN, datetime.now(timezone.utc)) |
| 31 | print(json.dumps(u, default=to_jsonable)) |
| 32 | |
| 33 | class ForgeEncoder(json.JSONEncoder): |
| 34 | def default(self, o: Any) -> Any: |
| 35 | try: |
| 36 | return to_jsonable(o) |
| 37 | except TypeError: |
| 38 | return super().default(o) |
| 39 | |
| 40 | print(json.dumps({"u": u}, cls=ForgeEncoder)) |
🔥
pro tip
At API boundaries prefer Pydantic model_dump(mode="json") over hand-rolled encoders.
csv DictReader / DictWriter
csv_dict.py
Python
| 1 | import csv |
| 2 | from pathlib import Path |
| 3 | |
| 4 | path = Path("users.csv") |
| 5 | rows = [ |
| 6 | {"id": "1", "name": "Ada", "role": "admin"}, |
| 7 | {"id": "2", "name": "Grace", "role": "user"}, |
| 8 | ] |
| 9 | |
| 10 | with path.open("w", newline="", encoding="utf-8") as f: |
| 11 | writer = csv.DictWriter(f, fieldnames=["id", "name", "role"], extrasaction="ignore") |
| 12 | writer.writeheader() |
| 13 | writer.writerows(rows) |
| 14 | |
| 15 | with path.open(newline="", encoding="utf-8") as f: |
| 16 | reader = csv.DictReader(f) |
| 17 | for row in reader: |
| 18 | print(row["id"], row["name"], row["role"]) |
| 19 | # row is dict[str, str | None] |
⚠
warning
Always open CSV files with newline="" (docs requirement) to avoid blank-line bugs on Windows.
Dialects & Excel Quirks
dialects.py
Python
| 1 | import csv |
| 2 | from pathlib import Path |
| 3 | |
| 4 | csv.register_dialect( |
| 5 | "pipe", |
| 6 | delimiter="|", |
| 7 | quotechar='"', |
| 8 | quoting=csv.QUOTE_MINIMAL, |
| 9 | skipinitialspace=True, |
| 10 | ) |
| 11 | |
| 12 | path = Path("table.pipe") |
| 13 | with path.open("w", newline="", encoding="utf-8") as f: |
| 14 | w = csv.writer(f, dialect="pipe") |
| 15 | w.writerow(["id", "name"]) |
| 16 | w.writerow([1, "Ada Lovelace"]) |
| 17 | |
| 18 | # Sniff dialect from a sample |
| 19 | sample = Path("users.csv").read_text(encoding="utf-8")[:1024] |
| 20 | try: |
| 21 | dialect = csv.Sniffer().sniff(sample, delimiters=",;\t|") |
| 22 | print(dialect.delimiter) |
| 23 | except csv.Error: |
| 24 | print("could not sniff") |
| 25 | |
| 26 | # Excel-friendly UTF-8 BOM |
| 27 | bom_path = Path("excel.csv") |
| 28 | with bom_path.open("w", newline="", encoding="utf-8-sig") as f: |
| 29 | csv.DictWriter(f, fieldnames=["a", "b"]).writeheader() |
| Dialect knob | Purpose |
|---|---|
| delimiter | Field separator |
| quotechar | Quoting character |
| quoting | QUOTE_MINIMAL/ALL/NONE/NONNUMERIC |
| escapechar | Escape when needed |
| doublequote | "" inside quoted fields |
Typed Round-Trip Pattern
roundtrip.py
Python
| 1 | import csv |
| 2 | import json |
| 3 | from dataclasses import dataclass |
| 4 | from pathlib import Path |
| 5 | |
| 6 | @dataclass |
| 7 | class Item: |
| 8 | sku: str |
| 9 | qty: int |
| 10 | price: float |
| 11 | |
| 12 | def items_to_csv(items: list[Item], path: Path) -> None: |
| 13 | with path.open("w", newline="", encoding="utf-8") as f: |
| 14 | w = csv.DictWriter(f, fieldnames=["sku", "qty", "price"]) |
| 15 | w.writeheader() |
| 16 | for it in items: |
| 17 | w.writerow({"sku": it.sku, "qty": it.qty, "price": f"{it.price:.2f}"}) |
| 18 | |
| 19 | def items_from_csv(path: Path) -> list[Item]: |
| 20 | out: list[Item] = [] |
| 21 | with path.open(newline="", encoding="utf-8") as f: |
| 22 | for row in csv.DictReader(f): |
| 23 | out.append(Item(row["sku"], int(row["qty"]), float(row["price"]))) |
| 24 | return out |
| 25 | |
| 26 | def items_to_json(items: list[Item], path: Path) -> None: |
| 27 | payload = [{"sku": i.sku, "qty": i.qty, "price": i.price} for i in items] |
| 28 | path.write_text(json.dumps(payload, indent=2), encoding="utf-8") |
| 29 | |
| 30 | items = [Item("A-1", 2, 9.5), Item("B-2", 1, 4.0)] |
| 31 | items_to_csv(items, Path("items.csv")) |
| 32 | assert items_from_csv(Path("items.csv"))[0].sku == "A-1" |
| 33 | items_to_json(items, Path("items.json")) |
Pitfalls
- JSON keys are always strings after loads — int keys become str
- csv reads everything as str — cast intentionally
- Huge JSON files: consider ijson; huge CSV: stream DictReader
- NaN/Infinity are non-standard JSON — allow_nan=False to reject
- Never use eval on JSON; always json.loads
pitfalls.py
Python
| 1 | import json |
| 2 | print(json.loads(json.dumps({1: "a"}))) # {"1": "a"} — key became str |
| 3 | print(json.dumps(float("nan"), allow_nan=True)) # NaN — not valid JSON RFC |
| 4 | # json.dumps(float("nan"), allow_nan=False) # ValueError |
JSON Lines (NDJSON)
For streaming logs and large datasets, prefer one JSON object per line.
ndjson.py
Python
| 1 | import json |
| 2 | from pathlib import Path |
| 3 | from collections.abc import Iterator |
| 4 | from typing import Any |
| 5 | |
| 6 | def write_ndjson(path: Path, rows: list[dict[str, Any]]) -> None: |
| 7 | with path.open("w", encoding="utf-8") as f: |
| 8 | for row in rows: |
| 9 | f.write(json.dumps(row, ensure_ascii=False) + "\n") |
| 10 | |
| 11 | def read_ndjson(path: Path) -> Iterator[dict[str, Any]]: |
| 12 | with path.open(encoding="utf-8") as f: |
| 13 | for line in f: |
| 14 | line = line.strip() |
| 15 | if line: |
| 16 | yield json.loads(line) |
| 17 | |
| 18 | write_ndjson(Path("events.ndjson"), [{"e": "click"}, {"e": "view"}]) |
| 19 | print(list(read_ndjson(Path("events.ndjson")))) |
csv.reader / writer
csv_list.py
Python
| 1 | import csv |
| 2 | from io import StringIO |
| 3 | |
| 4 | buf = StringIO() |
| 5 | w = csv.writer(buf) |
| 6 | w.writerow(["name", "score"]) |
| 7 | w.writerow(["Ada", 98]) |
| 8 | w.writerow(["name, with comma", 1]) |
| 9 | buf.seek(0) |
| 10 | for row in csv.reader(buf): |
| 11 | print(row) |
Performance Notes
- json.loads is fast enough for typical API payloads
- orjson / ujson can help hot paths — measure first
- For multi-GB CSV, stream; do not read().splitlines()
- Prefer list of dicts only when you need random access
Schema at the Boundary
Treat JSON/CSV as untrusted input. Parse, then validate into dataclasses or Pydantic models before business logic.
boundary.py
Python
| 1 | import json |
| 2 | from dataclasses import dataclass |
| 3 | |
| 4 | @dataclass(frozen=True) |
| 5 | class Payload: |
| 6 | id: int |
| 7 | name: str |
| 8 | |
| 9 | def parse_payload(raw: str) -> Payload: |
| 10 | data = json.loads(raw) |
| 11 | if not isinstance(data, dict): |
| 12 | raise ValueError("expected object") |
| 13 | return Payload(id=int(data["id"]), name=str(data["name"])) |
| 14 | |
| 15 | print(parse_payload('{"id": 1, "name": "Ada"}')) |
$Blueprint — Engineering Documentation·Section ID: PYTHON-JSON-CSV·Revision: 1.0
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.