If you have been concatenating file paths with string slicing or wrestling with os.path.join just to open a file, python pathlib is about to make your life dramatically easier. Introduced in Python 3.4 and part of the standard library, the pathlib module treats file system paths as objects rather than plain strings. That shift in thinking changes everything — your code becomes shorter, more readable, and works identically on Windows, macOS, and Linux without any extra effort on your part.

This tutorial walks through the pathlib module from the ground up. You do not need any prior experience with file handling in Python — just a working Python 3.4+ installation and the willingness to write a little code.

What Is the Python Path Class and Why Does It Exist

The heart of pathlib is the Path class. When you create a Path object, you get something that knows it represents a location on the file system. You can ask it questions — does this file exist? What is its extension? What directory does it live in? — and it answers them directly as attributes and methods, no helper functions required.

Before pathlib, the typical approach looked like this:

import os

base = "/home/user/projects"
file_path = os.path.join(base, "data", "report.csv")
filename = os.path.basename(file_path)
extension = os.path.splitext(filename)[1]

print(file_path)
print(filename)
print(extension)

That works, but it is verbose and you have to remember which function does what. Now look at the same thing with python pathlib:

from pathlib import Path

file_path = Path("/home/user/projects") / "data" / "report.csv"

print(file_path)
print(file_path.name)
print(file_path.suffix)

The / operator is overloaded on Path objects to join path segments — it reads exactly like a file path looks. file_path.name gives you the filename, file_path.suffix gives you the extension. No imports of multiple functions, no cryptic splitext tuples.

Creating Path Objects

You can create a Path from a string, from multiple string segments, or by using the special class methods that pathlib provides.

from pathlib import Path

# From a single string
p1 = Path("/tmp/myfile.txt")

# From multiple segments joined together
p2 = Path("/tmp", "subfolder", "myfile.txt")

# Current working directory
cwd = Path.cwd()

# Home directory of the current user
home = Path.home()

print(p1)
print(p2)
print(cwd)
print(home)
/tmp/myfile.txt
/tmp/subfolder/myfile.txt
/Users/rashmi/projects
/Users/rashmi

Path.cwd() and Path.home() are especially handy — they give you sensible starting points without hardcoding absolute paths, which is exactly what makes python pathlib code portable across machines.

On Windows, Path automatically returns a WindowsPath object that understands backslashes and drive letters. On Unix it returns a PosixPath. You never have to think about this — it just works.

Python Pathlib Exists and Checking Path Properties

Before you try to open or modify a file, you almost always want to check whether it actually exists. The pathlib module makes these checks trivially readable.

from pathlib import Path

p = Path("/tmp/sample.txt")

print(p.exists()) # True if the path exists at all
print(p.is_file()) # True only if it's a regular file
print(p.is_dir()) # True only if it's a directory
print(p.is_symlink()) # True if it's a symbolic link
False
False
False
False

Each of these is a method on the Path object itself — you are literally asking the path "are you a file?" rather than passing yourself into a separate utility function. This is the python pathlib philosophy in one line: the path object knows everything about itself.

You can combine these checks in natural if statements:

from pathlib import Path

config = Path.home() / ".config" / "app_settings.json"

if config.is_file():
 print("Config found, loading settings...")
else:
 print("No config found, using defaults.")

Python Pathlib Mkdir — Creating Directories Safely

Creating a directory hierarchy used to require os.makedirs with the right flags to avoid errors if the path already existed. Python pathlib wraps this into a single expressive call.

from pathlib import Path

output_dir = Path("output") / "reports" / "2024"

output_dir.mkdir(parents=True, exist_ok=True)

print(f"Directory created: {output_dir}")
print(f"Is directory: {output_dir.is_dir()}")
Directory created: output/reports/2024
Is directory: True

parents=True tells pathlib to create every intermediate directory that does not yet exist — like mkdir -p on the command line. exist_ok=True means if the directory is already there, just move on instead of raising a FileExistsError. Together these two arguments make mkdir safe to call unconditionally at the start of any script that writes output files.

Navigating Path Components With Stem, Suffix, Parent, and More

One of the most satisfying parts of python pathlib is how it breaks a path into its components. You do not parse strings or split on dots — you just read attributes.

from pathlib import Path

p = Path("/home/user/documents/budget_2024.xlsx")

print(p.name) # Full filename including extension
print(p.stem) # Filename without extension
print(p.suffix) # Extension including the dot
print(p.suffixes) # All extensions for multi-part names
print(p.parent) # The containing directory
print(p.parents[1]) # Two levels up
print(p.parts) # All components as a tuple
print(p.anchor) # Root, e.g. '/' on Unix
budget_2024.xlsx
budget_2024
.xlsx
['.xlsx']
/home/user/documents
/home/user
('/', 'home', 'user', 'documents', 'budget_2024.xlsx')
/

The stem and suffix attributes are particularly useful when you need to rename a file or generate an output filename derived from an input filename. For example, if you process data.csv and want to save the result as data_processed.csv:

from pathlib import Path

source = Path("data.csv")
output = source.parent / (source.stem + "_processed" + source.suffix)

print(output)
data_processed.csv

Reading and Writing Files With Python Read File Pathlib

Path objects can open files directly — you do not need to pass a string to open(). Even better, pathlib gives you convenience methods read_text, write_text, read_bytes, and write_bytes that handle opening and closing for you in a single call.

from pathlib import Path

notes = Path("notes.txt")

# Write a file in one line — creates it if it doesn't exist
notes.write_text("First line\nSecond line\nThird line\n")

# Read it back just as easily
content = notes.read_text()
print(content)
First line
Second line
Third line

For binary data — images, PDFs, pickled objects — swap in write_bytes and read_bytes:

from pathlib import Path

binary_file = Path("data.bin")
binary_file.write_bytes(b"\x00\xFF\x10\x20")

raw = binary_file.read_bytes()
print(raw)
b'\x00\xff\x10 '

When you need more control — encoding, buffering, appending — you can still use Path with the standard open() built-in:

from pathlib import Path

log = Path("app.log")

with log.open("a", encoding="utf-8") as f:
 f.write("Session started\n")

The open method on the Path object behaves exactly like the built-in open function but you do not need to convert the path to a string first. The python pathlib module integrates cleanly with all standard file I/O patterns.

Pathlib Glob Python — Finding Files With Patterns

Searching for files that match a pattern is one of the most common file system tasks in scripting and data processing. The glob method on a Path object returns an iterator of matching paths.

from pathlib import Path

project = Path(".")

# All Python files in the current directory
for py_file in project.glob("*.py"):
 print(py_file)
main.py
utils.py
config.py

The ** pattern means "any number of directories deep" — this is what you want for recursive searches:

from pathlib import Path

project = Path(".")

# Every .json file anywhere in the tree
for json_file in project.glob("**/*.json"):
 print(json_file)
config/settings.json
data/input/records.json
data/output/results.json

You can also use rglob as a shortcut — p.rglob("*.json") is equivalent to p.glob("**/*.json"):

from pathlib import Path

for csv in Path(".").rglob("*.csv"):
 print(csv.name, "—", csv.stat().st_size, "bytes")
sales_q1.csv — 4096 bytes
sales_q2.csv — 3820 bytes
raw_data.csv — 102400 bytes

stat() gives you a os.stat_result with file size, timestamps, and permissions — all accessible right through the pathlib interface.

Pathlib vs os.path — When to Use Which

The os.path module is not going away, and for very simple scripts a quick os.path.join is still fine. But for any code that does meaningful file system work, python pathlib wins on almost every dimension.

Taskos.pathpathlib
Join pathsos.path.join(a, b, c)Path(a) / b / c
Get filenameos.path.basename(p)p.name
Get extensionos.path.splitext(p)[1]p.suffix
Check existenceos.path.exists(p)p.exists()
Make directoriesos.makedirs(p, exist_ok=True)p.mkdir(parents=True, exist_ok=True)
Read fileopen(p).read()p.read_text()
List directoryos.listdir(p)p.iterdir()

The pathlib module shines brightest in larger codebases where paths are passed between functions. A Path object carries its own intelligence — any function that receives one can immediately call .exists(), .parent, .suffix without knowing where the path came from. A plain string carries nothing.

Renaming, Moving, and Deleting Files

Python pathlib also covers file operations that you might have previously handled with shutil or os.

from pathlib import Path

# Create a temp file to work with
original = Path("old_name.txt")
original.write_text("some content")

# Rename (also works as a move if you change the directory)
renamed = original.rename("new_name.txt")
print(renamed) # new_name.txt
print(renamed.exists()) # True
print(original.exists()) # False — the old path no longer exists

# Delete a file
renamed.unlink()
print(renamed.exists()) # False
new_name.txt
True
False
False

To remove an empty directory use rmdir(). For non-empty directory trees you still need shutil.rmtree — pathlib intentionally does not wrap that to avoid accidental data loss.

Full Working Example

This script scans a source directory for Markdown files, reads each one, counts the words, and writes a summary report to an output directory. It uses python pathlib throughout — mkdir, glob, read_text, write_text, stem, suffix, and exists all appear in real context.

from pathlib import Path
from datetime import datetime


def count_words(text: str) -> int:
 return len(text.split())


def generate_report(source_dir: Path, output_dir: Path) -> Path:
 output_dir.mkdir(parents=True, exist_ok=True)

 md_files = sorted(source_dir.glob("**/*.md"))

 if not md_files:
 print(f"No Markdown files found in {source_dir}")
 return

 lines = [
 f"# Word Count Report",
 f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
 f"Source: {source_dir.resolve()}",
 "",
 "| File | Words |",
 "|------|-------|",
 ]

 total_words = 0

 for md_file in md_files:
 content = md_file.read_text(encoding="utf-8")
 word_count = count_words(content)
 total_words += word_count
 relative = md_file.relative_to(source_dir)
 lines.append(f"| {relative} | {word_count} |")

 lines.append(f"| **TOTAL** | **{total_words}** |")
 lines.append("")
 lines.append(f"Scanned {len(md_files)} file(s).")

 report_path = output_dir / "word_count_report.md"
 report_path.write_text("\n".join(lines), encoding="utf-8")

 return report_path


# --- Setup: create sample files so the script is self-contained ---

docs = Path("sample_docs")
docs.mkdir(exist_ok=True)
(docs / "intro.md").write_text("# Introduction\n\nWelcome to the project. " * 10)
(docs / "guide.md").write_text("# Guide\n\nFollow these steps carefully. " * 15)

subdir = docs / "advanced"
subdir.mkdir(exist_ok=True)
(subdir / "deep_dive.md").write_text("# Deep Dive\n\nLet us explore further. " * 20)

# --- Run the report generator ---

report = generate_report(
 source_dir=Path("sample_docs"),
 output_dir=Path("output") / "reports",
)

if report and report.exists():
 print(f"Report written to: {report}")
 print()
 print(report.read_text())
Report written to: output/reports/word_count_report.md

# Word Count Report
Generated: 2024-06-15 11:42:07
Source: /Users/rashmi/projects/sample_docs

| File | Words |
|------|-------|
| advanced/deep_dive.md | 62 |
| guide.md | 52 |
| intro.md | 42 |
| **TOTAL** | **156** |

Scanned 3 file(s).