pathlib and Files

Updated

September 8, 2026

pathlib and Files

The boring way to touch disk in Python is pathlib.Path: a path is an object, / joins parts, and read_text / write_text carry an encoding. Run file examples inside tempfile.TemporaryDirectory so they create nothing you have to clean up.

Mental model

A path is a location (tickets/t7.txt), not the file’s bytes. Path stores that location. Joining is /, not string concatenation. Reading and writing text is read_text and write_text with encoding="utf-8" spelled out — never “whatever the locale is today.”

TemporaryDirectory makes a real directory, yields its path as a string, and deletes the tree when the with block ends. That is how every listing in this chapter stays runnable and clean.

Path is the default. Reach for os.path only when an old API demands a string and will not take a Path.

Worked examples

Case 1: Write a ticket, read it back

Save as save_ticket.py. write_text creates the file. read_text returns a str.

# save_ticket.py
from pathlib import Path
from tempfile import TemporaryDirectory


def main() -> None:
    with TemporaryDirectory() as raw:
        desk = Path(raw)
        ticket = desk / "ticket-7.txt"
        ticket.write_text("ticket 7 → table 12\n", encoding="utf-8")
        print(ticket.name)
        print(ticket.read_text(encoding="utf-8"), end="")


if __name__ == "__main__":
    main()

Run:

uv run python save_ticket.py

Output:

ticket-7.txt
ticket 7 → table 12

The temp directory is gone after main returns. The program still proved the round-trip.

Case 2: Directories, iterdir, and glob

Save as desk_tree.py. mkdir creates a folder. iterdir lists it. glob matches a pattern.

# desk_tree.py
from pathlib import Path
from tempfile import TemporaryDirectory


def main() -> None:
    with TemporaryDirectory() as raw:
        desk = Path(raw)
        tickets = desk / "tickets"
        tickets.mkdir()
        (tickets / "t1.txt").write_text("table 3\n", encoding="utf-8")
        (tickets / "t2.txt").write_text("table 11\n", encoding="utf-8")
        names = sorted(p.name for p in tickets.iterdir())
        print(names)
        for path in sorted(desk.glob("tickets/*.txt")):
            print(f"{path.name}: {path.read_text(encoding='utf-8').strip()}")


if __name__ == "__main__":
    main()

Run:

uv run python desk_tree.py

Output:

['t1.txt', 't2.txt']
t1.txt: table 3
t2.txt: table 11

Sort names if you care about stable output. Directory order is not a promise.

Case 3: Missing files fail out loud

Save as missing_ticket.py. read_text raises FileNotFoundError. Catch that exception. Do not return None as a secret signal.

# missing_ticket.py
from pathlib import Path
from tempfile import TemporaryDirectory


def main() -> None:
    with TemporaryDirectory() as raw:
        path = Path(raw) / "missing.txt"
        try:
            path.read_text(encoding="utf-8")
        except FileNotFoundError:
            print("no ticket file")


if __name__ == "__main__":
    main()

Run:

uv run python missing_ticket.py

Output:

no ticket file

Case 4: Nested paths need parents

Save as nested_shift.py. write_text does not create missing parent directories. mkdir(parents=True, exist_ok=True) does.

# nested_shift.py
from pathlib import Path
from tempfile import TemporaryDirectory


def main() -> None:
    with TemporaryDirectory() as raw:
        path = Path(raw) / "shifts" / "monday" / "note.txt"
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text("front desk\n", encoding="utf-8")
        print(path.parent.name)
        print(path.read_text(encoding="utf-8"), end="")


if __name__ == "__main__":
    main()

Run:

uv run python nested_shift.py

Output:

monday
front desk

The trap

Path / other looks like join. If other is absolute, the left side is discarded. This program does not write anywhere; it only prints what the join produced.

Save as abs_join.py:

# abs_join.py
from pathlib import Path


def main() -> None:
    desk = Path("/home/desk")
    wrong = desk / "/tickets/t7.txt"
    right = desk / "tickets" / "t7.txt"
    print(wrong)
    print(right)


if __name__ == "__main__":
    main()

Run:

uv run python abs_join.py

Output:

/tickets/t7.txt
/home/desk/tickets/t7.txt

The first line is not under the desk. A leading / on the right-hand part is a new root, not a child. Join relative pieces: "tickets" then "t7.txt".

The boring rule

  • Use Path. Join with / and relative names.
  • Always pass encoding="utf-8" to read_text, write_text, and open.
  • Create parent directories explicitly (mkdir(parents=True, exist_ok=True)).
  • Use TemporaryDirectory in examples and tests so leftover files are not the API.
  • Catch FileNotFoundError / PermissionError at the edge, not as except Exception.

Try this

  1. In save_ticket.py, write two tickets and print how many .txt files desk.glob("*.txt") finds.
  2. In desk_tree.py, add tickets / "archive" with mkdir and skip directories when you print tables (if path.is_file()).
  3. In nested_shift.py, try write_text before mkdir and catch FileNotFoundError.