PythonFile Paths (pathlib)

File Paths (pathlib)

Before pathlib, working with file paths in Python meant manipulating plain strings with the os.path module — joining them with os.path.join(), splitting them with os.path.splitext(), and so on. It worked, but it was easy to get wrong, especially across operating systems with different path separators. The pathlib module, introduced in Python 3.4, represents a filesystem path as an object with methods and properties, which makes path code shorter, more readable, and automatically portable between Windows and Unix-like systems.

Why `pathlib.Path` Over `os.path`
  • Object-oriented: a path is a Path object with methods, not a bare string you pass to free functions.

  • Cross-platform: Path automatically uses the correct separator (\ on Windows, / elsewhere) for you.

  • Chainable: methods and the / operator can be strung together to build up complex paths readably.

  • Readable: Path("data") / "raw" / "file.csv" reads far more clearly than nested os.path.join() calls.

Python
from pathlib import Path
import os

# pathlib
config_path = Path("app") / "config" / "settings.json"

# equivalent with os.path
config_path_old = os.path.join("app", "config", "settings.json")

print(config_path)
print(config_path_old)
print(type(config_path))
app\config\settings.json
app\config\settings.json
<class 'pathlib.WindowsPath'>
Note
The printed separator depends on the operating system you run this on (`\\` on Windows, `/` on Linux/macOS) — that's exactly the portability `pathlib` gives you for free. `Path` automatically returns a `WindowsPath` or `PosixPath` depending on the platform.
Creating Paths

A Path can be built from a string, and there are convenient class methods for common starting points like the current working directory or the user's home folder.

Python
from pathlib import Path

relative = Path("some/dir")
current_dir = Path.cwd()
home_dir = Path.home()

print(relative)
print(current_dir)
print(home_dir)
some\dir
C:\Users\ada\projects\myapp
C:\Users\ada
Joining Paths with `/`

pathlib overloads the division operator so that / joins path segments, reading almost exactly like the path itself. This replaces os.path.join() and avoids manually worrying about separators or duplicate slashes.

Python
from pathlib import Path
import os

base = Path("projects") / "myapp"
log_file = base / "logs" / "2026-07-06.log"
print(log_file)

# The equivalent in os.path requires nested calls
log_file_old = os.path.join("projects", "myapp", "logs", "2026-07-06.log")
print(log_file_old)
projects\myapp\logs\2026-07-06.log
projects\myapp\logs\2026-07-06.log
Tip
You can mix a `Path` with plain strings on either side of `/` — only one of the operands needs to already be a `Path` object for the operator to work.
Common `Path` Methods and Properties

Method / property

Description

.exists()

Returns True if the path refers to something that exists on disk.

.is_file()

Returns True if the path exists and is a regular file.

.is_dir()

Returns True if the path exists and is a directory.

.name

The final path component, e.g. "report.csv" for "data/report.csv".

.stem

The filename without its final suffix, e.g. "report".

.suffix

The file extension, e.g. ".csv".

.parent

The containing directory as a Path.

.mkdir()

Creates the directory. Use parents=True, exist_ok=True for safe, nested creation.

.glob()

Yields Path objects matching a wildcard pattern, e.g. "*.py".

.resolve()

Returns the absolute, fully resolved path (symlinks and .. resolved).

Python
from pathlib import Path

p = Path("data/report.csv")

print(p.name)
print(p.stem)
print(p.suffix)
print(p.parent)
print(p.exists())
print(p.resolve())
report.csv
report
.csv
data
False
C:\Users\ada\projects\myapp\data\report.csv
Creating Directories

.mkdir() creates a single directory, and raises FileExistsError if it's already there. Passing parents=True creates any missing parent directories along the way, and exist_ok=True makes the call a no-op instead of raising when the directory already exists — the combination is the safest default for "make sure this directory exists" code.

Python
from pathlib import Path

output_dir = Path("build") / "reports" / "2026"
output_dir.mkdir(parents=True, exist_ok=True)
print(output_dir.exists())
True
Warning
Without `exist_ok=True`, calling `.mkdir()` on a directory that already exists raises `FileExistsError`. Without `parents=True`, `.mkdir()` fails with `FileNotFoundError` if any parent directory in the path is missing.
Reading and Writing Without `open()`

For simple cases, Path objects can read and write file contents directly, without ever calling open() yourself. .read_text() and .write_text() handle opening, reading or writing, and closing the file in one call — and both accept an encoding argument just like open() does.

Python
from pathlib import Path

note = Path("note.txt")
note.write_text("Remember to review the pull request.\n", encoding="utf-8")

print(note.read_text(encoding="utf-8"))
Remember to review the pull request.
Note
For binary data, the equivalents are `.read_bytes()` and `.write_bytes()`. These are all great for quick, one-shot reads and writes, but if you need finer control — appending, streaming line-by-line, or seeking within a file — you'll still reach for `open()`, typically as `Path.open()`, which behaves just like the built-in `open()`.
Worked Example: Listing Files with `.glob()`

.glob() searches a directory using shell-style wildcard patterns and yields matching Path objects lazily. It's a quick way to find every file of a certain type inside a folder, without manually filtering a directory listing.

Python
from pathlib import Path

project_dir = Path("src")

for py_file in sorted(project_dir.glob("*.py")):
    size = py_file.stat().st_size
    print(f"{py_file.name} — {size} bytes")
app.py — 1204 bytes
config.py — 512 bytes
utils.py — 2048 bytes
Tip
Use `.glob("**/*.py")` (with `**`) to search recursively through every subdirectory instead of just the top level — this is called "rglob" and is also available directly as the `.rglob()` shortcut method.
  • pathlib.Path treats a filesystem path as an object with methods, replacing most os.path string manipulation.

  • The / operator joins path segments in a readable, cross-platform way.

  • .exists(), .is_file(), .is_dir(), .name, .stem, .suffix, and .parent cover the most common path questions.

  • .read_text() / .write_text() skip open() entirely for simple whole-file reads and writes.

  • .glob() and .rglob() find files matching a pattern without manual directory-walking code.