pathlib — the file api ai should reach for first
Path — the object that replaces a thousand string operations
Reading AI-generated Python, you'll see two completely different ways of working with files. The old one looks like this:
import os
log_path = os.path.join("data", "logs", "today.log")
if os.path.exists(log_path):
with open(log_path) as f:
text = f.read()
Five function calls, three string-joining steps, and you still have to remember which OS-specific separator to worry about. AI ships this shape constantly because it's been on the internet since 2007.
The new way looks like this:
from pathlib import Path
log_path = Path("data") / "logs" / "today.log"
if log_path.exists():
text = log_path.read_text()
Same job. Half the lines. No string mashing. The / operator joins
path segments correctly on every platform. .exists() is a method on
the path object, not a function in another module. .read_text()
opens, reads, and closes the file in one call.
The mental model
A Path object is a value that represents a filesystem location. It
doesn't matter whether the file exists yet — Path("anything") is
always a valid object. What you do with it next is what matters:
- Build new paths:
p / "subdir" / "file.txt" - Inspect without touching disk:
p.name,p.suffix,p.parent - Check the filesystem:
p.exists(),p.is_file(),p.is_dir() - Read/write:
p.read_text(),p.write_text("..."),p.read_bytes() - List children:
p.iterdir(),p.glob("*.csv")
Every one of those is a method on the object. There is no separate
os.path.something function to remember. The object knows what it
can do.
A worked example
The editor on the right uses Path end to end:
from pathlib import Path
log = Path("/tmp/run.log")
log.write_text("started\n")
log.write_text(log.read_text() + "finished\n")
print(log.read_text())
print("exists?", log.exists())
print("name:", log.name)
Line by line:
- Build a
Pathobject pointing at/tmp/run.log. The file doesn't have to exist yet. write_textcreates the file (or overwrites it) with the string"started\n".write_textagain — but this time the new content is the current text plus"finished\n". The file ends up with two lines. (You'd normally use"a"mode for true appending, but this composition shows thatread_textandwrite_textwork together cleanly.)- Print the final contents —
started\nfinished\n. .exists()returnsTruenow that we've written it..namereturns just the filename component,"run.log".
No open(...). No with block. No f.write(...). Three method
calls do what open + with + f.read + f.close did.
Where AI specifically gets this wrong
Three patterns to flag in code Cursor writes you.
One: os.path.join(...). Anytime you see os.path.join("a", "b"), that's a Path("a") / "b" waiting to happen. The pathlib
version is shorter, type-checked, and platform-correct. AI defaults
to os.path.join because most of its training data is older than
pathlib being stable. Replace it.
One-and-a-half: os.path.exists(p). Becomes p.exists(). Same
behavior, no extra import.
Two: building paths with + or f-strings. path + "/" + name is
broken on Windows and ugly everywhere. Path(path) / name is one
character shorter and works on every platform. Look for f"{folder}/ {file}" constructions in AI code — every one of those is a
candidate for the / operator on a Path.
Three: open(...) for tiny reads. When AI writes a four-line
with open(p) as f: text = f.read() block to read one config file,
it's Path(p).read_text() in disguise. The with block matters when
you're streaming or reading line by line — but for "load the whole
thing into a string," read_text is the move.
Run the editor. The Pyodide environment exposes a virtual /tmp, so
the file genuinely gets written and read inside the browser.