|$ 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

PythonJSONCSVIntermediate🎯Free Tools
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
1import json
2from pathlib import Path
3
4obj = {"name": "Ada", "ok": True, "n": 42, "tags": ["math", "code"]}
5s = json.dumps(obj, indent=2, sort_keys=True, ensure_ascii=False)
6print(s)
7data = json.loads(s)
8assert data["name"] == "Ada"
9
10path = Path("data.json")
11path.write_text(json.dumps(obj), encoding="utf-8")
12loaded = json.loads(path.read_text(encoding="utf-8"))
13
14# dump/load with file objects
15with path.open("w", encoding="utf-8") as f:
16 json.dump(obj, f, indent=2)
ParameterEffect
indent=2Pretty-print
sort_keys=TrueStable key order
ensure_ascii=FalseKeep Unicode as-is
separators=(',', ':')Compact output
default=fnHook for non-JSON types
parse_float=DecimalExact decimals
Encoders, Decoders & Dataclass Hooks
json_hooks.py
Python
1import json
2from dataclasses import dataclass, asdict, is_dataclass
3from datetime import datetime, timezone
4from enum import Enum
5from pathlib import Path
6from typing import Any
7
8class Role(Enum):
9 ADMIN = "admin"
10 USER = "user"
11
12@dataclass
13class User:
14 id: int
15 name: str
16 role: Role
17 created: datetime
18
19def 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
30u = User(1, "Ada", Role.ADMIN, datetime.now(timezone.utc))
31print(json.dumps(u, default=to_jsonable))
32
33class 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
40print(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
1import csv
2from pathlib import Path
3
4path = Path("users.csv")
5rows = [
6 {"id": "1", "name": "Ada", "role": "admin"},
7 {"id": "2", "name": "Grace", "role": "user"},
8]
9
10with 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
15with 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
1import csv
2from pathlib import Path
3
4csv.register_dialect(
5 "pipe",
6 delimiter="|",
7 quotechar='"',
8 quoting=csv.QUOTE_MINIMAL,
9 skipinitialspace=True,
10)
11
12path = Path("table.pipe")
13with 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
19sample = Path("users.csv").read_text(encoding="utf-8")[:1024]
20try:
21 dialect = csv.Sniffer().sniff(sample, delimiters=",;\t|")
22 print(dialect.delimiter)
23except csv.Error:
24 print("could not sniff")
25
26# Excel-friendly UTF-8 BOM
27bom_path = Path("excel.csv")
28with bom_path.open("w", newline="", encoding="utf-8-sig") as f:
29 csv.DictWriter(f, fieldnames=["a", "b"]).writeheader()
Dialect knobPurpose
delimiterField separator
quotecharQuoting character
quotingQUOTE_MINIMAL/ALL/NONE/NONNUMERIC
escapecharEscape when needed
doublequote"" inside quoted fields
Typed Round-Trip Pattern
roundtrip.py
Python
1import csv
2import json
3from dataclasses import dataclass
4from pathlib import Path
5
6@dataclass
7class Item:
8 sku: str
9 qty: int
10 price: float
11
12def 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
19def 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
26def 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
30items = [Item("A-1", 2, 9.5), Item("B-2", 1, 4.0)]
31items_to_csv(items, Path("items.csv"))
32assert items_from_csv(Path("items.csv"))[0].sku == "A-1"
33items_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
1import json
2print(json.loads(json.dumps({1: "a"}))) # {"1": "a"} — key became str
3print(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
1import json
2from pathlib import Path
3from collections.abc import Iterator
4from typing import Any
5
6def 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
11def 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
18write_ndjson(Path("events.ndjson"), [{"e": "click"}, {"e": "view"}])
19print(list(read_ndjson(Path("events.ndjson"))))
csv.reader / writer
csv_list.py
Python
1import csv
2from io import StringIO
3
4buf = StringIO()
5w = csv.writer(buf)
6w.writerow(["name", "score"])
7w.writerow(["Ada", 98])
8w.writerow(["name, with comma", 1])
9buf.seek(0)
10for 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
1import json
2from dataclasses import dataclass
3
4@dataclass(frozen=True)
5class Payload:
6 id: int
7 name: str
8
9def 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
15print(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.