Most Python code that deals with files and folders should nowadays use the pathlib module instead of the older os and os.path functions.
pathlib offers:
- Cleaner, more readable code
- Platform-independent path handling (
/works on Windows too) - Object-oriented interface
- Chainable operations
- Better exception messages
:::{tip}
Use pathlib for all new code. Only use os when you specifically need low-level operations (os.scandir, os.walk, os.chmod, process handling, etc.) or when supporting very old Python versions (< 3.4).
:::
Why use pathlib?
-
Object-Oriented: Paths are objects, not just strings, so you can call methods directly on them.
-
Readable: The / operator for joining paths is much cleaner than nested os.path.join calls.
-
Cross-platform: It handles the difference between Windows () and Unix (/) separators automatically.
from pathlib import PathYou usually only need this one import for most file-system tasks.
# Common useful paths
home = Path.home()
cwd = Path.cwd() # current working directory
desktop = Path.home() / "Desktop"
document = Path.home() / "Documents"
config = Path.home() / ".config" / "myapp" / "settings.toml"
project = Path("/home/maikel/projects/my-awesome-app")
readme = project / "README.md"
folder = Path.home() / "projects"
# All entries (files + folders)
for item in folder.iterdir():
print(item.name)
# Only file names as list
files = [p.name for p in folder.iterdir()]
print(files)
# Only Python files
py_files = [p.name for p in folder.glob("*.py")]
print(py_files)
# Recursively find all .md files
markdowns = list(folder.rglob("*.md"))pathlib does not have a method to change the current directory — you still use os for that:
import os
os.chdir(Path.home() / "projects")
# or
os.chdir(str(Path.home() / "projects")) # sometimes needed for older codeBut you can easily get & show the current directory as a Path:
print(Path.cwd())p = Path("notes.txt")
print(p.exists()) # True / False
print(p.is_file()) # True if regular file
print(p.is_dir()) # True if directory
print(p.is_symlink()) # True if symbolic linkfrom datetime import datetime
photo = Path("vacation-2025.jpg")
# File size in bytes
size = photo.stat().st_size
print(f"Size: {size:,} bytes")
# Last modification time
mtime = datetime.fromtimestamp(photo.stat().st_mtime)
print(mtime.strftime("%Y-%m-%d %H:%M:%S"))# Create directories (like mkdir -p)
report_folder = Path.home() / "Reports" / "2026" / "Q1"
report_folder.mkdir(parents=True, exist_ok=True)
# Create an empty file
(Path("temp.txt")).touch()
# Delete file
(Path("old.log")).unlink(missing_ok=True)
# Delete directory (must be empty)
(Path("empty_folder")).rmdir()
# Rename / move
source = Path("draft.md")
source.rename(source.with_name("final.md"))
# or move to different folder
source.rename(Path.home() / "Archive" / source.name)# Very readable and platform-safe
logfile = (
Path.home()
/ ".local"
/ "share"
/ "myapp"
/ "logs"
/ f"app-{datetime.now():%Y%m%d}.log"
)
# Alternative style
config = Path("config") / "prod" / "database.ini"| Task | Old (os / os.path) |
Modern (pathlib) |
Notes |
|---|---|---|---|
| Current working directory | os.getcwd() |
Path.cwd() |
— |
| User home directory | os.path.expanduser("~") |
Path.home() |
Platform-independent |
| Join path components | os.path.join(a, b, c) |
Path(a) / b / c or Path(a, b, c) |
/ operator is very readable |
| Base filename | os.path.basename(p) |
Path(p).name |
Includes extension |
| Directory part | os.path.dirname(p) |
Path(p).parent |
Returns a Path object |
| Exists (file or dir) | os.path.exists(p) |
Path(p).exists() |
— |
| Is regular file? | os.path.isfile(p) |
Path(p).is_file() |
Does not follow symlinks |
| Is directory? | os.path.isdir(p) |
Path(p).is_dir() |
Does not follow symlinks |
| File size (bytes) | os.path.getsize(p) |
Path(p).stat().st_size |
Requires .stat() |
| Last modification time | os.path.getmtime(p) |
Path(p).stat().st_mtime |
Unix timestamp (float) |
| List directory contents | os.listdir(path) |
[p.name for p in Path(path).iterdir()] |
iterdir() → Path objects |
| Pattern matching (glob) | glob.glob("*.py") |
Path().glob("*.py") |
Returns iterator of Path objects |
| Recursive glob | — | Path().rglob("*.py") |
Like **/*.py in shell |
| Delete / remove file | os.remove(p) |
Path(p).unlink(missing_ok=True) |
missing_ok prevents exceptions |
| Create directory tree | os.makedirs(path, exist_ok=True) |
Path(path).mkdir(parents=True, exist_ok=True) |
parents=True ≈ mkdir -p |
# Find largest file in folder
biggest = max(folder.iterdir(), key=lambda p: p.stat().st_size if p.is_file() else 0)
print(f"Largest file: {biggest.name} – {biggest.stat().st_size:,} bytes")
# Backup all .py files
for py in folder.rglob("*.py"):
backup = py.with_suffix(".py.bak")
py.replace(backup) # atomic move/rename
# Create dated report folder if needed
today = datetime.now().strftime("%Y-%m-%d")
(Path("reports") / today).mkdir(exist_ok=True)