Reading and writing structured data on disk. You touched files in the intro tutorials; here we focus on patterns that scale to small apps and CLI tools.
pathlib for all path work
Stop hand-joining strings with slashes. Path reads clearly and handles OS differences.
from pathlib import Path
root = Path(__file__).resolve().parent
data_dir = root / "data"
data_dir.mkdir(exist_ok=True)
output = data_dir / "report.csv"
output.write_text("id,namen1,Alicen", encoding="utf-8")
JSON for config and interchange
JSON is the default for config files and API payloads. Always specify encoding.
import json
from pathlib import Path
def load_json(path: Path) -> dict:
with path.open(encoding="utf-8") as handle:
return json.load(handle)
def save_json(path: Path, data: dict) -> None:
text = json.dumps(data, indent=2, ensure_ascii=False)
path.write_text(text + "n", encoding="utf-8")
CSV for tabular exports
Use the csv module instead of splitting strings manually – commas inside fields will catch you out.
import csv
from pathlib import Path
def write_contacts(path: Path, rows: list[dict[str, str]]) -> None:
fieldnames = ["name", "email"]
with path.open("w", encoding="utf-8", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
Environment-specific config
Keep secrets out of files you commit. Load defaults from JSON and override with environment variables.
import os
from pathlib import Path
def load_settings() -> dict:
base = load_json(Path("config/settings.json"))
base["database_url"] = os.environ.get("DATABASE_URL", base["database_url"])
return base
Validate after loading
Files lie. Check required keys before you use them.
REQUIRED = {"host", "port", "debug"}
def validate_settings(data: dict) -> dict:
missing = REQUIRED - data.keys()
if missing:
raise ValueError(f"settings missing keys: {sorted(missing)}")
return data
Atomic writes for important files
Write to a temporary file, then rename. If the process dies mid-write, you do not leave a half-empty config behind.
import tempfile
from pathlib import Path
def atomic_write(path: Path, content: str) -> None:
with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False, dir=path.parent) as tmp:
tmp.write(content)
temp_name = Path(tmp.name)
temp_name.replace(path)
Next: HTTP clients and APIs – fetching remote data, posting JSON, and handling failures without taking down your whole script.

