with, Files, and Paths
with always closes. pathlib Path names a file. utf-8 turns text into bytes. This is how read and write tools work.
A file is text (or bytes) stored under a name. Agent tools like read_file and write_file are file work. If you open a file and never close it, the handle can stay locked. On a laptop that looks like “the file is busy.” In a long agent, it looks like a leak.
with means: open, do the work, then always close — even if an error happens. That close step is why we use with instead of a bare open.
with runs close even if work fails. That is how read and write tools should treat files.
Open, work, always closepathlib is the standard way to name files. A Path is an object for a file name. You can ask for the name, check if it exists, and read or write text. You do not glue strings with + and hope the slash is right.
with always closes
Think of with as a promise: when the indented block ends, Python runs the close step.
Here is a tiny object that prints open and close so you can see the order:
class Box:
def __enter__(self):
print(class="tok-s">"open")
return self
def __exit__(self, *unused):
print(class="tok-s">"close")
return False
with Box():
print(class="tok-s">"work")Output order is open, work, close. If work raised an error, close would still run. __enter__ and __exit__ are the protocol. You do not write them for real files. open already has them. The demo is so you believe “always close.”
On a real computer, a file looks like this:
with open(class="tok-s">"note.txt", class="tok-s">"w", encoding=class="tok-s">"utf-8") as f:
f.write(class="tok-s">"goal: find weather" + chr(10))"w"means write (replace the file)"r"means read"a"means appendencoding="utf-8"stores letters as bytes the whole world can read
utf-8 is the usual encoding. Text in Python is str. On disk it is bytes. utf-8 is the bridge. If you skip encoding on some systems, a café in the text becomes mojibake.
as f names the file object. f.write writes. f.read reads. Prefer pathlib for whole-file text: Path.write_text / read_text. They still close for you.
pathlib Path
Path("note.txt") is a path object. You do not have to call open yourself for simple text.
| Code | What it does |
|---|---|
Path("note.txt") | A path named note.txt |
p.name | The file name |
p.exists() | True if that file is there |
p.write_text(s, encoding="utf-8") | Write the whole string |
p.read_text(encoding="utf-8") | Read the whole string |
p.with_suffix(".json") | Same name, new ending |
p.unlink() | Delete the file |
/ on a Path joins parts: Path("logs") / "run1.json". It works on Windows and Linux. String glue does not.
Never let a model pick a path like ../secrets.txt without a check. Keep file tools inside one folder you control. Resolve the path, then check that it still starts with the allowed folder. This lesson only warns. The check is a later safety habit: p = (root / name).resolve() then reject if root is not a parent.
Bytes vs text
stris text you can print and slicebytesis raw data, likeb"hello"s.encode("utf-8")turns text into bytesb.decode("utf-8")turns bytes into text
HTTP bodies and hashes use bytes. Prompts use text. Convert at the edge. Do not pass bytes into a prompt assembler. Do not pass a str into sha256 without encoding.
write_text wants str. write_bytes wants bytes. Mixing them is a TypeError.
Check exists before a read tool returns a surprise. If the file is missing, return {"ok": False, "error": "not found"} instead of letting FileNotFoundError kill the loop. You will wrap that in try in the next lesson. Pathlib still helps: p.exists() is a bool you can test in if.
"a" appends. "w" replaces the whole file. A write tool that was meant to append a log line but opened with "w" will wipe the run. That is a one-character bug. Prefer write_text for replace, and read-modify-write for small files: read the string, add a line, write the whole string. For huge logs, append mode on a laptop is better. Here, whole-string is clearer.
Join paths with / on a Path: root / "traces" / "run.json". Then resolve and check the result still sits under root. Model-supplied ".." is the attack. The check is string prefix or .parents. Do it every time a tool takes a path.
Common mistakes
openwithoutwithand withoutclose.- Forgetting
encoding="utf-8". - Letting the model choose
../paths. - Using
+to join path parts. - Reading bytes and treating them as a prompt string.
Run to execute this in your browser. Nothing is sent to a server.
This editor has a small virtual disk. Writing agent-note.txt is safe here. Then we delete it. On your laptop, wrap real open in with, or use Path.write_text. Both close for you. Catch file errors in the next lesson.
How agents use this
A read tool is Path(name).read_text(encoding="utf-8") plus a folder check. A write tool is write_text. with is how you close a handle if you use open. Convert to bytes only when a library asks for bytes (hashes, some HTTP bodies). If you skip close, files stay locked. If you skip utf-8, accents break.
Traces saved as JSON are files: dump to a string, write the string, later read and load. The JSON lesson and this lesson are one pipeline. The agent should not hold the only copy of a long run in memory if you need to debug after a crash. Write the trace. Close the file. Open it in a test.
Model-chosen paths are a tool-safety problem, not a pathlib problem. Pathlib makes the check easier because you have .resolve() and parts. Still check. A string open(user_path) is how secrets leave the machine.
Check your understanding