JEPA4Japan · tutorials

Files, Paths, CSV, and JSON

1,333 words 7 min read #Python

Read and write local data safely with pathlib, with, standard-library context managers, CSV, and JSON.

Course progress Course outline 24 of 24 lessons available

Give values a safe shelf

Values in memory disappear when a program stops. A file lets the next run find them again—but files live beyond your program’s control. They can be missing, malformed, or only partly written.

  1. Path where the shelf is
  2. with open, use, then close
  3. Format how stored text is shaped
A path finds the file, a context manages it, and a format explains it.

Treat storage as a boundary: decode and validate data before the rest of the program trusts it.

Build paths and close files

pathlib.Path joins path parts using the current operating system’s rules:

from pathlib import Path

path = Path("data") / "sessions.json"
print(path)
print(path.name)
print(path.parent)
print(path.is_absolute())

Typical macOS or Linux output:

data/sessions.json
sessions.json
data
False

Windows normally displays backslashes. A relative path depends on the process’s current working directory, which may differ from the script’s directory. Accept or derive a clear base path rather than guessing.

exists(), is_file(), and is_dir() answer different questions. A check can guide your choice, but it cannot promise that a later open will succeed; the filesystem may change between the two operations.

Use with so an open file closes even when an exception leaves the block. Always name the text encoding:

path = Path("plan.txt")

with path.open("w", encoding="utf-8") as file:
    file.write("Python: 30 minutes\n")

with path.open("r", encoding="utf-8") as file:
    print(file.read(), end="")

"r" reads, "w" replaces and first truncates old content, and "a" appends. Appending suits independent records; appending a second complete JSON document does not make one valid JSON document.

Let CSV and JSON speak for themselves

CSV fields can contain commas, so do not parse them with split(","). Open real CSV files with newline="" and let csv control quoting and newlines:

import csv

with Path("sessions.csv").open("w", encoding="utf-8", newline="") as file:
    writer = csv.DictWriter(file, fieldnames=["topic", "minutes"])
    writer.writeheader()
    writer.writerow({"topic": "Data, files", "minutes": 40})

The data row becomes "Data, files",40. csv.DictReader returns field values as strings, so convert and validate numeric fields.

  1. dict / list Python containers
  2. json.dump encode as JSON text
  3. json.load decode into Python values
  4. Validate check the promised shape
Valid JSON syntax is only halfway; decoded data still needs a shape check.

JSON objects become dictionaries, arrays become lists, null becomes None, and booleans become True or False. Strings and numbers map naturally. A tuple encodes as an array and returns as a list; Path, set, and arbitrary objects are not supported by default.

Validate after decoding and recover at the boundary

json.load() can successfully decode "hello", even if your program needs a list of session dictionaries. Check both the top level and every item:

def validate_session(value):
    if not isinstance(value, dict) or set(value) != {"topic", "minutes"}:
        raise ValueError("a session needs topic and minutes")

    topic = value["topic"]
    minutes = value["minutes"]
    if not isinstance(topic, str) or not topic.strip():
        raise ValueError("topic must be non-empty text")
    if isinstance(minutes, bool) or not isinstance(minutes, int) or minutes < 0:
        raise ValueError("minutes must be a non-negative integer")

    return {"topic": topic.strip(), "minutes": minutes}

The separate bool check matters because bool is a subclass of int. Return a clean dictionary so later code receives one trustworthy shape.

  1. Missing maybe return an empty collection
  2. Malformed add useful decode context
  3. Unexpected let permission and bugs surface
Recover only where you understand the failure and can choose a sensible next step.

Catch FileNotFoundError only if absence is an ordinary starting state. Convert json.JSONDecodeError to a clearer boundary error if useful. Do not catch every Exception and quietly return empty data; permission failures and programming bugs mean something else.

Build a persistent study repository

Save this complete standard-library project as study_repository.py:

import csv
import json
from pathlib import Path
from tempfile import TemporaryDirectory


def validate_session(value):
    if not isinstance(value, dict) or set(value) != {"topic", "minutes"}:
        raise ValueError("a session needs topic and minutes")
    topic = value["topic"]
    minutes = value["minutes"]
    if not isinstance(topic, str) or not topic.strip():
        raise ValueError("topic must be non-empty text")
    if isinstance(minutes, bool) or not isinstance(minutes, int) or minutes < 0:
        raise ValueError("minutes must be a non-negative integer")
    return {"topic": topic.strip(), "minutes": minutes}


def load_sessions(path):
    try:
        with path.open("r", encoding="utf-8") as file:
            data = json.load(file)
    except FileNotFoundError:
        return []
    except json.JSONDecodeError as error:
        raise ValueError(f"invalid JSON at line {error.lineno}") from error

    if not isinstance(data, list):
        raise ValueError("session data must be a list")
    return [validate_session(item) for item in data]


def save_sessions(path, sessions):
    cleaned = [validate_session(item) for item in sessions]
    path.parent.mkdir(parents=True, exist_ok=True)
    temporary = path.with_suffix(path.suffix + ".tmp")
    try:
        with temporary.open("w", encoding="utf-8") as file:
            json.dump(cleaned, file, ensure_ascii=False, indent=2)
            file.write("\n")
        temporary.replace(path)
    finally:
        temporary.unlink(missing_ok=True)


def export_csv(path, sessions):
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8", newline="") as file:
        writer = csv.DictWriter(file, fieldnames=["topic", "minutes"])
        writer.writeheader()
        writer.writerows(sessions)


def main():
    with TemporaryDirectory() as directory:
        base = Path(directory)
        json_path = base / "sessions.json"
        csv_path = base / "sessions.csv"
        source = [
            {"topic": " Python ", "minutes": 45},
            {"topic": "Data, files", "minutes": 25},
        ]

        assert load_sessions(json_path) == []
        save_sessions(json_path, source)
        restored = load_sessions(json_path)
        export_csv(csv_path, restored)

        assert restored[0]["topic"] == "Python"
        assert '"Data, files",25' in csv_path.read_text(encoding="utf-8")
        print(f"Round trip: {len(restored)} sessions, 70 minutes")
        print("Tests passed.")


if __name__ == "__main__":
    main()

Output:

Round trip: 2 sessions, 70 minutes
Tests passed.

Validation happens before writing. The neighboring temporary file greatly reduces the chance of leaving a half-written destination, but it is not a universal promise about crashes, durability, permissions, or several writers.

Three tiny missions and the handoff

  1. Append diary. Write two independent lines with mode "a"; run twice and verify four lines. Explain why this design would not work for a single JSON document.
  2. CSV import. Use csv.DictReader, require exactly topic and minutes, convert minutes, and validate each row. Test a topic containing a comma.
  3. Keep the old file. Save valid JSON, then try saving negative minutes. Confirm ValueError occurs and the old destination still loads.

Sharp corners: "w" truncates immediately, relative paths depend on the working directory, decoded JSON can have the wrong shape, and broad exception handling can hide real failures.

  • I can combine and inspect paths with Path.
  • I can use with, UTF-8, and the correct file mode.
  • I know why real CSV files use newline="".
  • I can map JSON values and validate the decoded structure.
  • I recover only from expected failures at a boundary.
  • My temporary-directory project completes its round trip.
  • I completed the three tiny missions.

Next, you will place valid state and its related behavior together inside classes and objects.