pathlib and Files
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.pyOutput:
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.pyOutput:
['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.pyOutput:
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.pyOutput:
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.pyOutput:
/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"toread_text,write_text, andopen. - Create parent directories explicitly (
mkdir(parents=True, exist_ok=True)). - Use
TemporaryDirectoryin examples and tests so leftover files are not the API. - Catch
FileNotFoundError/PermissionErrorat the edge, not asexcept Exception.
Try this
- In
save_ticket.py, write two tickets and print how many.txtfilesdesk.glob("*.txt")finds. - In
desk_tree.py, addtickets / "archive"withmkdirand skip directories when you print tables (if path.is_file()). - In
nested_shift.py, trywrite_textbeforemkdirand catchFileNotFoundError.