If you still join paths with os.path.join and check files with os.path.exists, try pathlib.Path. It turns a path into an object with built-in methods for reading, writing, creating directories, and browsing files, so your code is shorter, reads naturally, and is less prone to separator mistakes between Windows and Linux. This applies whenever you write a file-processing script, an internal tool, or any Python code that touches the filesystem.

The Problem

With os.path, a path is just a string. You must remember the exact argument order of join, join the separators yourself, and call yet another function whenever you want to check existence, get a filename, or create a directory. Code ends up long and scattered: even a simple job like “find every .txt file in a tree and write the results to a new file” needs several functions combined, and one forgotten / is enough to break the path.

import os
root = os.path.join("du-an", "data", "raw")
files = [f for f in os.listdir(root) if f.endswith(".txt")]
out = os.path.join("du-an", "ket-qua.txt")
with open(out, "w", encoding="utf-8") as fh:
fh.write("\n".join(files))

The Solution

pathlib.Path wraps a path in an object. The / operator joins paths intuitively, and methods like exists(), mkdir(), glob(), and read_text() live right on that object. The same code runs on both Windows and Linux because Path handles the separators itself.

from pathlib import Path
root = Path("du-an") / "data" / "raw"
files = sorted(p.name for p in root.glob("*.txt"))
out = Path("du-an") / "ket-qua.txt"
out.write_text("\n".join(files), encoding="utf-8")
print(out.exists(), out.suffix, out.parent.name)

Beyond this example, the most useful methods are mkdir(parents=True, exist_ok=True) for creating a whole directory tree, rglob("*.csv") for recursive browsing, with_suffix(".json") for changing an extension, relative_to() for a relative path, and resolve() for a normalized absolute path. When an old function insists on a string, just call str(path) — no need to rewrite all your code.

Apply It to Your Project

Do not convert a whole codebase at once. Start with new scripts or the hardest-to-read spots: path joining, directory browsing, or temporary file writes. Replace each one with Path, re-run the tests or run it by hand once to confirm the result is unchanged, then spread gradually to other modules. For projects with existing functions that take string paths, you can still pass a Path in and convert at the boundary with str() when needed, so the switch is almost risk-free.

Related Posts

FAQ

Does pathlib fully replace os.path?

Almost, but it does not have to. pathlib handles most path operations well, while some legacy system functions such as os.listdir or os.environ keep their own roles. Use Path for paths and keep os for everything else.

Does Path make code slower?

Not noticeably for ordinary scripts. Path is just a wrapper around a path string, and the cost of creating the object is tiny compared with the time spent actually reading, writing, or browsing files. Only if you create millions of Path objects inside a very hot loop would you need to measure with timeit.

How do I get a string back from a Path?

Use str(path) when a function requires a string, or path.as_posix() when you need to be sure the separator is / — for example when writing paths into a shared config file.

Should I use Path or os.path in an old project?

Use Path for new code and for code you are already changing. For old code that runs fine, there is no need to convert everything at once — only switch when you touch that section for another reason, to avoid unnecessary churn.

💬 Have you tried this in your own project? If you hit an error or found a neater way, leave a comment below — I read them all and will fold the best ideas into the post.

References