Paths and Files

Build paths with pathlib instead of string joins, read and write text with an explicit encoding, and check the right thing.

SyntaxPath(*pathsegments).read_text(encoding=None, errors=None, newline=None) -> str

A 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · python
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.

Example · python
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.

Example · python
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

  • Be the first to comment on this lesson.