Plate 58
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 Challa4 min read
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
| Arm | Pattern |
|---|---|
os.stat(path) | classic |
os.lstat(path) | no symlink follow |
Path.stat() reuse | Path objects kept |
Path(p).stat() fresh | construct each call |
open + fstat | fd metadata |
Lab topology
Script: lab-evidence/106-stat-vs-path-stat/results/run_lab.py.
Lead table — 2000 files (p50)
| Arm | kstats/s |
|---|---|
| os.stat | 605.1 |
| os.lstat | 604.06 |
| Path.stat reuse | 462.24 |
| open+fstat | 250.15 |
| Path fresh each time | 148.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)
| n | os.stat | Path reuse | Path fresh |
|---|---|---|---|
| 500 | 592.33 | 564.32 | 172.24 |
| 2000 | 605.1 | 462.24 | 148.94 |
| 8000 | 445.44 | 389.87 | 148.29 |
Reading it
- Hot inventory loops: prefer
os.stator reusedPathobjects. - Avoid
Path(path_str).stat()inside tight loops (alloc tax). lstat≈staton regular files; uselstatwhen symlinks matter.fstatneeds 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
statvslstat. - Statting after every open when
os.DirEntry.stat()fromscandirwould do (see scandir lab). - Cold dentry cache vs warm — we warm with a warmup round.
Reproduce
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.
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/.
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