Python files & pathlib, explained
Reading and writing files is everyday work, and modern Python makes it clean: context
managers guarantee files are closed, and pathlib replaces fragile path-string manipulation
with real objects. Here's the idiomatic way to handle both.
Open files with a context manager
Always open files with with. The context manager closes the file automatically — even if an
exception is raised mid-read — so you never leak handles or lose buffered writes.
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read()
# file is guaranteed closed here, exception or not
Specify encoding explicitly (almost always "utf-8"); relying on the platform default is a
classic source of "works on my machine" text bugs.
File modes and reading patterns
The mode string controls intent: r read, w write (truncates!), a append, x create-or-
fail, and a trailing b for binary. There are several ways to read.
with open("data.txt") as f:
whole = f.read() # entire file as one string
# f.readlines() → list of lines
with open("big.log") as f:
for line in f: # streams line by line — memory-efficient
process(line.rstrip("\n"))
Iterating the file object is the right way to handle large files — it never loads everything into memory.
Writing files
w overwrites, a appends. write doesn't add newlines, so include them yourself or use
writelines/print(..., file=f).
with open("out.txt", "w", encoding="utf-8") as f:
f.write("first line\n")
f.writelines(f"{n}\n" for n in range(3))
with open("out.txt", "a") as f:
f.write("appended\n")
Be careful: opening with "w" truncates the file the moment it's opened, even before you
write anything.
pathlib: paths as objects
pathlib.Path replaces os.path string functions with an object-oriented API. The /
operator joins paths portably across operating systems.
from pathlib import Path
p = Path("data") / "reports" / "june.txt" # OS-correct separator
p.name # 'june.txt'
p.stem # 'june'
p.suffix # '.txt'
p.parent # Path('data/reports')
p.exists() # True/False
No more os.path.join, os.path.basename, os.path.splitext — it's all methods and
properties on one object.
Reading and writing with Path
Path has convenience methods for the common one-shot read/write, so you don't even need
open for simple cases.
from pathlib import Path
p = Path("notes.txt")
p.write_text("hello\n", encoding="utf-8") # write whole file
text = p.read_text(encoding="utf-8") # read whole file
data = Path("img.png").read_bytes() # binary read
For larger or streaming work, p.open() returns a normal file object you use with with.
Globbing and traversing directories
pathlib makes finding and walking files concise. glob matches a pattern in one directory;
rglob recurses.
from pathlib import Path
for py in Path("src").rglob("*.py"): # all .py files, recursively
print(py)
Path("out").mkdir(parents=True, exist_ok=True) # create dirs safely
Path("old.txt").unlink(missing_ok=True) # delete if present
mkdir(parents=True, exist_ok=True) and unlink(missing_ok=True) handle the "already
exists / doesn't exist" cases without try/except.
Recap
Open files with with so they're closed automatically, and always pass
encoding="utf-8". Know the modes (r/w/a/x, +b), remember w truncates, and
iterate the file object to stream large files line by line. Prefer pathlib.Path over
os.path: join with /, inspect with .name/.stem/.suffix/.parent, read and write
with read_text/write_text/read_bytes, and find files with glob/rglob. It's the
modern, portable, readable way to handle the filesystem.