ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
Topics
sre111localhost lab102performance70python58javascript47bundle size36benchmarking35node.js35web development21typescript19benchmark16content creation15All topics
About
Loading article…

ShopperCove

Hands-on notes on developer tools, written after the tool has actually been run.

All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove

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
  12. FAQ
  13. Is pathlib slower than os.path?
  14. Is Path.rglob slower than os.walk?
  15. Should I stop using pathlib?
  1. Blog

pathlib vs os.path in Python: Is pathlib Slower? Tested

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

30 September 2026·6 min read

Summary

Disclosure

This post contains affiliate links. If you buy through them, I may earn a commission at no extra cost to you. I only make firsthand claims about products I have actually used.

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
  12. FAQ
  13. Is pathlib slower than os.path?
  14. Is Path.rglob slower than os.walk?
  15. Should I stop using pathlib?

Intro — what this post promises

pathlib is slower than os.path mainly when building paths: on Python 3.13.5, Path / ran about 0.48M joins/s against 2.2M for os.path.join (about 0.22x), while exists and stat were only about 15-20% slower because the system call dominates. Keep pathlib for readable app code, and fall back to os.path.join or os.walk only where a profiler shows path building in a hot loop.

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:

  • 01shoppercove.comtempfile NamedTemporaryFile localhost lab
  • 02shoppercove.comatomic rename vs overwrite localhost lab
  • 03shoppercove.comfsync vs fdatasync localhost lab
  • 04shoppercove.composix fadvise sequential vs random localhost lab
  • 05shoppercove.comdataclass vs slots vs dict localhost lab
  • 06shoppercove.comjson vs orjson vs msgpack localhost lab
  • 07shoppercove.compython re vs str methods localhost lab
  • 08shoppercove.comzstd 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.

FAQ

Is pathlib slower than os.path?

For joins, clearly: Path / about 0.48M ops/s, Path.joinpath 0.98M, os.path.join 2.20M. For Path.exists and Path.stat the gap is small (about 0.86x and 0.83x of os.path).

Is Path.rglob slower than os.walk?

On this roughly 92-entry tree, yes: os.walk with endswith did about 9.5k scans/s vs about 4.0k for Path.rglob('*.txt'). glob.iglob landed next to rglob.

Should I stop using pathlib?

No. A single join in a request handler doesn't matter. The tax shows up in tight loops building millions of paths, such as ingest or scan jobs.

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/.

Explore the product

Untested by us. Check what's included, the license and refund terms before you buy.

Ready Client Sites for prebuilt websites

See Ready Client Sites details

Notes when a lab post goes up

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

Explore the product

Untested by us. Check what's included, the license and refund terms before you buy.

Ready Client Sites for prebuilt websites

See Ready Client Sites details

Keep reading

  • 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.

    Summary1 Oct 20264 min read

  • 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.

    Summary1 Oct 20263 min read

  • 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.

    Summary1 Oct 20264 min read