ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
About

Plate 58

  1. Blog
  2. /Observability & SRE

os.stat vs Path.stat vs lstat: Localhost Lab

Hands-on os.stat vs pathlib.Path.stat vs os.lstat: real metadata stats/s on many regular files, measured on Linux localhost in this hands-on lab for SREs.

Aditya Challa·30 September 2026·4 min read

Lab
On this page
  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table — 2000 files (p50)
  5. Scale sketch (kstats/s)
  6. Reading it
  7. Pathlib still fine
  8. DirEntry hint
  9. Symlink footnote
  10. Caching reality
  11. Pitfalls
  12. Reproduce
  13. Limits
  14. Takeaway

Intro — what this post promises

Stat many existing regular files via os.stat, pathlib.Path.stat, and os.lstat. This lab reports stats/s on Linux localhost.

It is not pathlib-vs-ospath path join / existence (lab 47). Here the only question is metadata syscall throughput.

Related links:

  • pathlib vs ospath localhost lab
  • configparser vs json localhost lab
  • html escape vs manual localhost lab
  • queue vs deque handoff localhost lab
  • stringio vs list join localhost lab
  • zlib vs gzip compress localhost lab
  • difflib vs set ops localhost lab
  • groupby vs manual localhost lab

Lab honesty (1 Oct 2026 IST): Python 3.13.5. Affiliates: 0. No Docker. Regular files only (no symlinks — so lstat ≈ stat).

Verdict up front (2000 files): os.stat ~605.1 kstats/s; os.lstat ~604.06; Path.stat reuse ~462.24; fresh Path(p).stat() ~148.94; open+fstat ~250.15.


Arms

ArmPattern
os.stat(path)classic
os.lstat(path)no symlink follow
Path.stat() reusePath objects kept
Path(p).stat() freshconstruct each call
open + fstatfd metadata

Lab topology

n in {500, 2000, 8000} small files under results/tree/ · 7 rounds · p50
metric: stats/s = n / p50_s

Script: lab-evidence/106-stat-vs-path-stat/results/run_lab.py.


Lead table — 2000 files (p50)

Armkstats/s
os.stat605.1
os.lstat604.06
Path.stat reuse462.24
open+fstat250.15
Path fresh each time148.94

Reused Path objects stay close to os.stat. Constructing a new Path per call drops to about ~25% of os.stat here.


Scale sketch (kstats/s)

nos.statPath reusePath fresh
500592.33564.32172.24
2000605.1462.24148.94
8000445.44389.87148.29

Reading it

  • Hot inventory loops: prefer os.stat or reused Path objects.
  • Avoid Path(path_str).stat() inside tight loops (alloc tax).
  • lstat ≈ stat on regular files; use lstat when symlinks matter.
  • fstat needs an fd — fine if you already opened; expensive if open is only for metadata.

Pathlib still fine

For application code that stats occasionally, Path ergonomics win. This lab only bites when you stat thousands of paths per request/scan. Lab 47 remains the join/exists story — do not conflate.


DirEntry hint

If you are already walking with os.scandir, DirEntry.stat() can avoid extra path lookups. This lab isolates pure path-based stat calls — combine with the scandir lab when designing a tree walker.


Symlink footnote

We intentionally used regular files so lstat and stat should match. Introduce symlinks and stat follows while lstat reports the link inode — correctness diverges even when throughput looks identical on this fixture. Always choose the API for the symlink policy you want, not for a microbench dead heat.


Caching reality

Numbers here are warm-cache. First pass over a huge cold tree is dominated by disk/metadata FS latency; Python API choice shrinks to noise. Re-run on your storage class before setting SLOs off this localhost SSD/pagecache profile.


Pitfalls

  • Benchmarking Path reuse when production builds Path fresh each time.
  • Following symlinks unintentionally with stat vs lstat.
  • Statting after every open when os.DirEntry.stat() from scandir would do (see scandir lab).
  • Cold dentry cache vs warm — we warm with a warmup round.

Reproduce

python3 lab-evidence/106-stat-vs-path-stat/results/run_lab.py

Evidence: summary.json, summary.txt, tree/.

Batching stats behind a small worker pool rarely helps on pure metadata — the GIL and syscall path dominate before CPU parallelism pays. Measure before introducing threads solely for stat.


Limits

One Linux box. Local filesystem (not NFS). No symlink set. Warm page/dentry cache after warmup.


Takeaway

At 2000 files, os.stat ~605.1 kstats/s ≈ lstat; reused Path.stat ~462.24; fresh Path construction ~148.94. Keep Path objects or use os.stat in hot metadata scans.

os.statpath.statos.lstatfile metadatapathliblocalhost labsrestats/s

Lab evidence

What I found running this

Lab 1 Oct 2026 IST. Python 3.13.5. n=2000: os.stat 605.1 kstats/s; lstat 604.06; Path reuse 462.24; Path fresh 148.94. Not lab 47 join/exists. Affiliates: 0. Evidence: lab-evidence/106-stat-vs-path-stat/.

Notes when a lab post goes up

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

Related links

  • Plate 12

    islice vs list Slice Windows: Localhost Lab

    Hands-on itertools.islice vs list slice window lab: real ops/s taking ranges from sequences, measured on Linux localhost in this hands-on lab for SREs.

    Observability & SRE · 1 Oct 2026

  • Plate 07

    heapq.merge vs sorted(chain): Localhost Lab

    Hands-on heapq.merge vs sorted(chain) multi-way merge: real records/s on pre-sorted lists, measured on Linux localhost today in this hands-on lab for SREs.

    Observability & SRE · 1 Oct 2026

  • Plate 88

    mmap Write vs pwrite Region: Localhost Lab

    Hands-on mmap MAP_SHARED write+msync vs pwrite region update: real MB/s with durability labels, measured on Linux localhost in this hands-on lab for SREs.

    Observability & SRE · 1 Oct 2026

On this page

  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table — 2000 files (p50)
  5. Scale sketch (kstats/s)
  6. Reading it
  7. Pathlib still fine
  8. DirEntry hint
  9. Symlink footnote
  10. Caching reality
  11. Pitfalls
  12. Reproduce
  13. Limits
  14. Takeaway
All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove