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.
4 min read
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:
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.
| 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 |
Script: lab-evidence/106-stat-vs-path-stat/results/run_lab.py.
| 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.
| 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 |
os.stat or reused Path objects.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.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.
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.
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.
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.
stat vs lstat.os.DirEntry.stat() from scandir would do (see scandir lab).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.
One Linux box. Local filesystem (not NFS). No symlink set. Warm page/dentry cache after warmup.
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
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/.
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.
Lab1 Oct 20264 min read
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.
Lab1 Oct 20264 min read
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.
Lab1 Oct 20264 min read