Walking trees and file metadata

pathlib · Advanced · 11 min

Cleanup jobs, disk-usage reports and incremental builds need to visit every file with its metadata.

Path.walk() (3.12+) is pathlib's version of os.walk. It yields (dirpath, dirnames, filenames) for each directory, with dirpath as a Path and the other two as lists of names:

for root, dirs, files in Path("proj").walk():
    dirs[:] = [d for d in dirs if d not in {".git", "node_modules"}]

Editing dirs in place prunes the walk, which is much faster than filtering rglob results after the fact because pruned directories are never listed. Use top_down=False to visit children first (needed for deleting directories), and on_error= to handle unreadable folders; by default errors are ignored.

Each file's metadata comes from path.stat():

  • st_size in bytes
  • st_mtime modification time as a POSIX timestamp; compare with time.time() or convert with datetime.fromtimestamp(ts, tz=timezone.utc)

A typical maintenance job, "delete logs older than 30 days", combines walk or glob, stat().st_mtime and unlink.

p.stat() follows symlinks; p.lstat() (or stat(follow_symlinks=False)) describes the link. p.owner() and p.group() give names on Unix, and p.chmod(0o644) changes permissions.

Example

import os
import time
from pathlib import Path

for f in ["logs/app.log", "logs/old/2023.log", ".git/HEAD"]:
    Path(f).parent.mkdir(parents=True, exist_ok=True)
    Path(f).write_text("x" * 100, encoding="utf-8")
month_ago = time.time() - 31 * 86400
os.utime("logs/old/2023.log", (month_ago, month_ago))

sizes, stale = {}, []
for root, dirs, files in Path(".").walk():
    dirs[:] = [d for d in dirs if not d.startswith(".")]     # prune .git
    for name in files:
        st = (root / name).stat()
        sizes[str(root / name)] = st.st_size
        if st.st_mtime < time.time() - 30 * 86400:
            stale.append(root / name)
print(sorted(sizes))      # ['logs/app.log', 'logs/old/2023.log']
print(stale)              # [PosixPath('logs/old/2023.log')]

Remember

  • Path.walk() (3.12+) yields (dirpath, dirnames, filenames) like os.walk
  • Prune by mutating dirnames in place; it skips whole subtrees
  • stat().st_size and st_mtime drive size and age based jobs

Watch out for

  • Using rglob('*') and filtering out .git afterwards on huge trees; the excluded folders are still fully scanned.

Check yourself

  1. Why is pruning dirnames in Path.walk faster than filtering rglob('*') results?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/pathlib.html#pathlib.Path.walk