Home Tutorials Standard Library

Standard Library

pathlib in Python: File Paths as Objects, Not Strings

Pyford Notes July 1, 2026 7 min read
Key points
  • Path objects know their parts — .name, .stem, .suffix, .parent — without string splitting.
  • The / operator joins path segments cross-platform: base / "subdir" / "file.txt".
  • path.read_text() and path.write_text() handle open/close automatically.
  • path.glob("*.py") and path.rglob("**/*.py") return lazy iterators of matching paths.

Importing pathlib

pathlib has been part of the standard library since Python 3.4. You rarely need the full module namespace — importing Path directly covers almost every use case:

from pathlib import Path

# On all platforms, Path() gives you the right concrete type
p = Path(".")
print(type(p))  # PosixPath on Linux/macOS, WindowsPath on Windows

The concrete PosixPath and WindowsPath types are selected automatically. You write code against the Path interface and it behaves correctly on every operating system.

Q: How do I create a Path object?

Pass a string, another Path, or any combination to the Path() constructor. Multiple arguments are joined automatically:

from pathlib import Path

home   = Path("/home/user")
config = Path("/home/user/.config/app/settings.toml")

# Path parts are accessible as attributes
print(config.name)    # settings.toml
print(config.stem)    # settings
print(config.suffix)  # .toml
print(config.parent)  # /home/user/.config/app

# Absolute vs relative
rel = Path("reports/2026/q1.csv")
print(rel.parts)      # ('reports', '2026', 'q1.csv')

These attributes eliminate the need for os.path.basename(), os.path.splitext(), and os.path.dirname() entirely.

The / operator is overloaded on Path objects to join segments. This is the idiomatic way to construct new paths and it works identically on all platforms — no need to choose between / and \:

from pathlib import Path

base = Path("/var/data")

logs    = base / "logs"
report  = base / "output" / "report.csv"
archive = base / "archive" / "2026" / "january.tar.gz"

print(report)   # /var/data/output/report.csv
print(archive)  # /var/data/archive/2026/january.tar.gz

# Resolve the current script's directory
here  = Path(__file__).parent
sibling = here / "utils.py"

You can also call path.resolve() to get the absolute, canonical path with all symlinks resolved, and path.relative_to(base) to express a path relative to a given ancestor.

Q: How do I read and write files with pathlib?

Path objects have high-level convenience methods that open, read or write, and close the file in a single call:

from pathlib import Path

p = Path("notes.txt")

# Write text (creates or overwrites)
p.write_text("Hello, pathlib!\n", encoding="utf-8")

# Read text back
content = p.read_text(encoding="utf-8")
print(content)  # Hello, pathlib!

# Binary equivalents
p.write_bytes(b"\x89PNG\r\n\x1a\n")
data = p.read_bytes()

# Create parent directories if missing
output = Path("build/dist/bundle.js")
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text("console.log('hello');")

For larger files where you want to stream content line by line, use p.open() as a context manager — it returns a standard file object exactly like the built-in open().

Q: How do I find files matching a pattern?

path.glob(pattern) searches the immediate directory and yields matching Path objects. path.rglob(pattern) descends recursively:

from pathlib import Path

project = Path(".")

# All Python files in the current directory only
for f in project.glob("*.py"):
    print(f.name)

# All Python files anywhere under the project
for f in project.rglob("*.py"):
    print(f)

# Collect into a sorted list
py_files = sorted(project.rglob("*.py"))
print(f"Found {len(py_files)} Python files")

# Multiple extensions using a set comprehension
sources = {p for p in project.rglob("*") if p.suffix in {".py", ".pyi"}}

Both methods return lazy generators — they do not load all results into memory. Use list() or sorted() only when you need random access or a stable ordering.

Q: How do I check whether a path exists or get file metadata?

Path exposes existence and stat information as simple method calls:

from pathlib import Path

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

print(p.exists())      # True or False
print(p.is_file())     # True only if it exists and is a regular file
print(p.is_dir())      # True only if it exists and is a directory

# File size and timestamps via os.stat_result
stat = p.stat()
print(stat.st_size)    # size in bytes
print(stat.st_mtime)   # last-modified time as a Unix timestamp

# Safe operations
p.unlink(missing_ok=True)   # delete; no error if already gone
p.parent.mkdir(parents=True, exist_ok=True)  # create directory tree

Q: Should I still use os.path?

For new code, prefer pathlib. The object-oriented interface is more readable and the methods cover the full lifecycle of a path without importing multiple modules. That said, os.path is not going anywhere, and many third-party libraries still accept strings. You can convert a Path to a string at the boundary with str(p) or pass it directly — most standard-library functions accept Path objects natively since Python 3.6.

Taskos.path stylepathlib style
Join segmentsos.path.join(a, b)a / b
Get filenameos.path.basename(p)p.name
Get extensionos.path.splitext(p)[1]p.suffix
Get parent diros.path.dirname(p)p.parent
Check existenceos.path.exists(p)p.exists()
Read all textopen(p).read()p.read_text()
Cross-platform tip Never hard-code forward slashes or backslashes in path strings. Using the / operator on Path objects guarantees the correct separator for the running operating system, and Path.resolve() handles drive letters and UNC paths on Windows without any conditional logic in your code.