ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
About

Plate 53

  1. Blog

pathlib vs os.path vs str: Path Ops Lab

A hands-on Linux localhost benchmark of pathlib and os.path for joins, filesystem checks, stats, and recursive scans.

Aditya Challa·30 September 2026·5 min read

Summary
On this page
  1. Intro — what this post promises
  2. What we are not claiming
  3. Lab topology
  4. Lead table — path join (p50 ops/s)
  5. Exists and stat
  6. Glob / walk — count `*.txt`
  7. Constructor overhead (bonus)
  8. Pitfalls
  9. When to pick what
  10. Reproduce
  11. Closing

Intro — what this post promises

Is pathlib.Path “just as fast” as os.path? Folklore says yes for modern Python. This lab times join, exists, stat, and glob/walk on a small real tree under /tmp on Linux localhost.

Arms:

  1. Join: os.path.join, string '/' concat, Path.joinpath, Path /.
  2. Exists / missing: os.path.exists, Path.exists, os.stat try/except.
  3. Stat / size: os.stat, Path.stat, os.path.getsize.
  4. Walk: Path.rglob('*.txt'), glob.iglob(..., recursive=True), os.walk count.

Related links:

  • tempfile NamedTemporaryFile localhost lab
  • atomic rename vs overwrite localhost lab
  • fsync vs fdatasync localhost lab
  • posix fadvise sequential vs random localhost lab
  • dataclass vs slots vs dict localhost lab
  • json vs orjson vs msgpack localhost lab
  • python re vs str methods localhost lab
  • zstd vs gzip vs lz4 compression localhost lab

Lab honesty (1 Oct 2026 IST): Python 3.13.5. Tree ~92 entries at /tmp/pathlab_tree. Affiliates: 0. Hot-cache microbench — not NFS / cold-page cold-start.

Verdict up front: join is where pathlib hurts. Path / ~0.48M/s vs os.path.join ~2.20M (~0.22×) vs raw str slash ~20.4M. exists ~0.86× and stat ~0.83× vs os.path — modest. os.walk count (~9.5k/s) beat rglob (~4.0k/s) on this tree.


What we are not claiming

  • pathlib is “bad” — ergonomics and type safety still win for app code.
  • str slash is portable — it is a Unix-shaped microbench cheat.
  • These numbers transfer to network filesystems or antivirus-scanned Windows paths.
  • A single join in a request handler matters — it does not. Tight loops (millions of path builds) do.

Lab topology

Tree: /tmp/pathlab_tree  (~92 files/dirs, nested a/b/c)
Join: 300k ops × 7 repeats (p50)
exists/stat: 100k ops × 7
glob/walk: 200–400 full scans × 5
Metric: p50 wall → ops/s

The tree is rebuilt at the start of run_lab.py so paths are real and exists hits. Join arms construct the same logical path under the tree root. Script: lab-evidence/47-pathlib-vs-ospath/results/run_lab.py.


Lead table — path join (p50 ops/s)

Armops/sns/opvs os.path.join
str '/' concat20 421 345499.3×
os.path.join2 204 0634541.00×
Path.joinpath982 27010180.45×
Path / (truediv)478 36020900.22×

Path / allocates and normalizes PosixPath objects (~2090 ns/op here). Fine in app code; painful in a tight inner loop that only needs a string for open(). Prefer joinpath over chained / if you stay on Path — still ~0.45× os.path.join on this box.


Exists and stat

Armops/svs baseline
os.path.exists (hit)707 4401.00×
Path.exists (hit)606 9460.86×
os.stat try (exists)725 2821.03×
os.path.exists (miss)821 352—
Path.exists (miss)567 037—
os.stat760 4061.00×
Path.stat632 7710.83×
os.path.getsize716 7350.94×

Syscall dominates; pathlib’s Python wrapper is a ~15–20% tax here — not a cliff like join. Missing-path exists was slightly faster for os.path (no inode payload) and slightly slower for pathlib on this run — treat that as noise band, not a product claim.


Glob / walk — count *.txt

Armscans/snotes
os.walk + endswith9 507count 80 matches
glob.iglob recursive4 072stdlib glob
Path.rglob('*.txt')3 985materialize list
Path.glob under a/13 503smaller subtree

On this ~92-entry tree, a tight os.walk loop was ~2.4× rglob. Larger trees and pattern complexity will shift absolute numbers; the relative tax of Path object churn remains the story. glob.iglob landed next to rglob (~4.1k/s) when both materialize matches.


Constructor overhead (bonus)

Armops/s
str identity35.4M
Path(root)1.74M
Path(root, 'a', '1', 'f1.txt')1.23M

Building Path objects is cheap next to disk — expensive next to string join in a hot path. If you construct once and reuse, cost amortizes; if you rebuild Path on every row of a large scan, you pay the join cliff again.


Pitfalls

  1. Benchmarking only exists/stat and concluding “pathlib is free” — join is the cliff.
  2. Using str slash in production Windows code — portability tax.
  3. Timing one cold walk — we use repeats; first-call noise is real on bigger trees.
  4. Comparing rglob list materialization to a streaming walk — fair for “give me all matches,” not for early-exit search.
  5. Over-optimizing one join in a web handler — measure the whole request; this lab is for ingest/scan loops.

When to pick what

NeedPrefer
App / library path logicpathlib
Hot join loop → string for syscallos.path.join or careful str
Massive recursive scanos.walk / os.scandir
Readable one-off scriptsPath — clarity wins

Reproduce

python3 lab-evidence/47-pathlib-vs-ospath/results/run_lab.py
# rebuilds /tmp/pathlab_tree; writes summary.json + summary.txt

Evidence: /workspace/lab-evidence/47-pathlib-vs-ospath/results/.


Closing

pathlib is not free on join: Path / ~0.48M vs os.path.join ~2.2M (~0.22×) vs str ~20M on this box. exists/stat stay in the ~0.83–0.86× band. Walking: os.walk ~9.5k/s vs rglob ~4.0k/s on a ~92-entry tree. Use Path for clarity; drop to os.path / os.walk when a profiler shows path construction in the hot path — and measure, don’t folklore.

pathlib vs os.pathpath joinos.path.existspathlib globpython pathslocalhost labsrefilesystem

Lab evidence

What I found running this

Lab 1 Oct 2026 IST. Python 3.13.5; tree ~92 entries under /tmp/pathlab_tree. Join: str slash 20.4M; os.path.join 2.20M; Path.joinpath 0.98M; Path/ 0.48M (~0.22x). exists: os.path 707k vs pathlib 607k (~0.86x). stat: os.stat 760k vs Path.stat 633k (~0.83x). rglob *.txt 3985/s vs os.walk 9507/s. Affiliates: 0. Evidence: lab-evidence/47-pathlib-vs-ospath/.

Notes when a lab post goes up

Occasional email for new hands-on reviews. No sequence and no sponsors.

Related links

  • Plate 17

    platform vs os.uname Inventory: Localhost Lab

    Hands-on platform.platform vs os.uname host inventory lab: real ops/s plus cache notes, measured on Linux localhost today in this hands-on lab for SREs.

    1 Oct 2026

  • Plate 75

    uuid.uuid4 vs uuid.uuid1: Localhost Lab

    Hands-on uuid.uuid4 vs uuid.uuid1 ID generation lab: real ops/s plus version/node checks, measured on Linux localhost today in this hands-on lab for SREs.

    1 Oct 2026

  • Plate 50

    signal vs threading.Event Wakeup: Localhost Lab

    Hands-on signal SIGUSR1 vs threading.Event wakeup lab: real p50 latency in microseconds, measured on Linux localhost today in this hands-on lab for SREs.

    1 Oct 2026

On this page

  1. Intro — what this post promises
  2. What we are not claiming
  3. Lab topology
  4. Lead table — path join (p50 ops/s)
  5. Exists and stat
  6. Glob / walk — count `*.txt`
  7. Constructor overhead (bonus)
  8. Pitfalls
  9. When to pick what
  10. Reproduce
  11. Closing
All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove