Hands-on fnmatch.filter vs re.compile name-list filtering measured on Linux localhost.
4 min read
Filter a large list of basenames with shell globs (fnmatch.filter / fnmatchcase) vs re.compile. This lab reports names/s on ~52700 synthetic names on Linux localhost.
It is not a filesystem walk (glob vs rglob vs walk) and not general string search (re vs str). Patterns run against in-memory names only.
Related links:
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Affiliates: 0. No Docker. Regexes are hand-translated equivalents of the globs (\Z-anchored).
Verdict up front (*.py): fnmatch.filter ~9.03 Mnames/s vs re.match ~8.28 M vs re.search ~3.07 M. Multi-glob any-of: re alternation ~1.95 M/s vs fnmatch loop ~1.65 M/s.
| Arm | Pattern |
|---|---|
fnmatch.filter(names, pat) | C-accelerated batch filter |
fnmatchcase loop | per-name case-sensitive |
re.compile(...).match | anchored regex |
re.compile(...).search | search (can be costlier with .*) |
| multi | several globs vs one alternation |
Script: lab-evidence/93-fnmatch-vs-re/results/run_lab.py.
| Pattern | fnmatch.filter | fnmatchcase | re.match | re.search |
|---|---|---|---|---|
*.py | 9.03 | 4.22 | 8.28 | 3.07 |
test_* | 11.62 | 4.92 | 10.24 | 9.86 |
app_*.json | 13.07 | 5.31 | 12.61 | 12.11 |
pkg.module_*.py | 12.56 | 5.24 | 12.62 | 13.96 |
*.py / *.json / test_*)| Arm | Mnames/s |
|---|---|
| fnmatch any (loop) | 1.65 |
| re alternation | 1.95 |
fnmatch.filter is the convenience winner for one shell glob over a list — often tied with or ahead of a careful re.match.fnmatchcase loops pay Python call overhead (~2× slower than filter here).re.search with leading .* can lose to match / fnmatch — compile thoughtfully.Use fnmatch.filter in tools and one-off filters — readable and fast enough. Reach for re.compile when patterns are dynamic, numerous, or already regex; keep the compiled object outside the loop.
fnmatch.translate can turn a glob into a regex string for re.compile. This lab used hand-written equivalents for clarity. If you generate regex from globs at runtime, compile once outside the name loop — the compile tax will otherwise dominate.
fnmatch.filter walks the list in C and appends matches — that is why it beat a Python fnmatchcase loop by ~2× on every pattern here. If you already iterate names for other reasons, fnmatchcase inline can still be fine; for “give me all *.py,” call filter.
All arms agreed on match counts per pattern (e.g. *.py and app_*.json hits matched across fnmatch and re). Throughput differences are engine cost, not divergent semantics on these fixtures.
re.search(r".*\.py") without \Z / fullmatch semantics.fnmatch on every os.walk name without measuring — different problem than this list filter.fnmatch vs fnmatchcase differ on case-insensitive platforms.Evidence: summary.json, fixture_names.txt sample.
One Linux box. Synthetic basenames. Not pathlib.match / recursive glob. Regexes hand-written, not fnmatch.translate microbench.
On ~52700 names, fnmatch.filter ~9.03 Mnames/s for *.py matched re.match ~8.28 M. For multi-glob, compiled alternation ~1.95 M/s edged the fnmatch any-loop. Default to fnmatch for shell-shaped filters; compile re when the hot path is many patterns or regex features.
Lab evidence
Lab 1 Oct 2026 IST. Python 3.13.5. n≈52700. *.py: fnmatch.filter 9.03 Mnames/s; re.match 8.28; re.search 3.07. Multi: re_alt 1.95 vs fnmatch_any 1.65. Not glob walk / re-vs-str string labs. Affiliates: 0. Evidence: lab-evidence/93-fnmatch-vs-re/.
Hands-on tarfile vs zipfile create+extract lab: real MB/s on a mixed small-file fixture (uncompressed tar vs zip), measured on Linux localhost for SREs.
Lab1 Oct 20264 min read
Hands-on os.scandir vs listdir vs Path.iterdir lab: real entries/s for names and is_file on a synthetic tree, measured on Linux localhost (lab) for SREs.
Lab1 Oct 20264 min read
Hands-on glob.glob vs Path.rglob vs os.walk lab: real files/s for recursive file listing on a modest fixture tree, measured on Linux localhost for SREs.
Lab1 Oct 20264 min read