Paths and Files
Build paths with pathlib instead of string joins, read and write text with an explicit encoding, and check the right thing.
Path(*pathsegments).read_text(encoding=None, errors=None, newline=None) -> strA path is not a string. It has a parent, a name, a stem, a suffix and parts, and joining two of them is a platform-specific operation. pathlib.Path models that; "logs" + "/" + name models a guess.
Building paths
Path("exports") / "2026" / "orders.csv" is the shorthand for Path("exports").joinpath("2026", "orders.csv") — the same call via __truediv__. Useful attributes: .name, .stem, .suffix, .parent, .parts, plus .with_name() and .with_suffix() for deriving a sibling. Every stdlib function that takes a path accepts a Path, so there is no need to convert back to str.
Reading and writing
p.read_text(encoding=None, errors=None, newline=None) is the shorthand for opening the file, reading it and closing it; p.write_text(data, ...) is the same for writing. The longhand is with p.open(encoding="utf-8") as f: f.read(), which is what you want for large files or line-by-line work. Always pass encoding: the default is the platform's, which differs between your laptop and the container. The newline parameter was added to write_text in 3.10 and to read_text in 3.13.
Exists is not the same as writable
p.exists() answers one narrow question. As of 3.14 it returns False rather than raising for an inaccessible path, so a permissions problem looks exactly like a missing file; use p.stat() when you need the real reason. Writing needs the parent directory to exist, and write_text will not create it. Checking first then writing is also a race — in a job that runs twice, just write and handle the exception.
Example
When to use it
- Build an output path from a directory, a date and a filename without string concatenation.
- Write a generated file so that readers never see it half-written.
- Read a configuration file with an explicit encoding so it behaves the same in the container.
- Collect every .log file under a tree in a deterministic order for a batch job.
More examples
Create the directory, then write atomically
The standard shape for any file another process reads: a cache, an export, a generated config. parents=True creates intermediate directories and exist_ok=True makes the call idempotent, which is what you want in a job that may run twice. replace() overwrites silently; rename() raises on Windows if the target exists.
from pathlib import Path
out = Path("exports") / "2026" / "orders.json"
out.parent.mkdir(parents=True, exist_ok=True) # write_text will not do this
# Write beside the target, then rename. Path.replace() is atomic on the same
# filesystem, so a concurrent reader sees either the old file or the new one,
# never a half-written one.
tmp = out.with_name(out.name + ".tmp")
tmp.write_text('{"rows": 0}\n', encoding="utf-8")
tmp.replace(out)Finding files, in a defined order
Use this to collect inputs for a batch job. The sorted() is not decoration: filesystem iteration order is not defined, so without it the job processes files in a different order on a different machine and any bug that depends on order becomes unreproducible.
from pathlib import Path
root = Path("logs")
# iterdir() is one level down; glob() takes a pattern; rglob("*.log") is the
# shorthand for glob("**/*.log") - the same recursive walk.
for p in sorted(root.rglob("*.log")): # sorted(): directory order is undefined
print(p, p.stat().st_size)
# A Path sorts as text and goes anywhere a str path goes.
newest = max(root.glob("*.log"), key=lambda p: p.stat().st_mtime, default=None)
print(newest)Encoding and errors on a file that is not yours
errors="replace" substitutes a replacement character for undecodable bytes instead of raising UnicodeDecodeError, which is right for a feed you must process and wrong for data you must not corrupt. Decide which case you are in, explicitly, rather than inheriting whatever the platform does.
from pathlib import Path
src = Path("vendor-feed.csv")
# With no encoding argument the platform default is used, which differs between
# a developer laptop and a container image. Name the encoding; name the error
# policy too when you cannot trust the producer.
text = src.read_text(encoding="utf-8", errors="replace")
# newline arrived in write_text in 3.10 and in read_text in 3.13; "\n" forces
# Unix line endings regardless of the platform writing the file.
Path("clean.csv").write_text(text, encoding="utf-8", newline="\n")
Discussion