fnmatch Asterisk Matches Across Path Separators, Unlike glob (tested)
In a shell, and in Python's glob module, * stops at a path separator -- temp/*.txt only matches files directly inside temp/. We measured fnmatch.fnmatch() on the same pattern and it doesn't stop at all.
1. The same pattern, different reach
import fnmatch
candidates = ["temp/a.txt", "temp/sub/b.txt", "temp/sub/deep/d.txt"]
pattern = "temp/*.txt"
for c in candidates:
print(c, "->", fnmatch.fnmatch(c, pattern))
temp/a.txt -> True
temp/sub/b.txt -> True
temp/sub/deep/d.txt -> True
Measured: every candidate matches, including files two directories deeper than temp/. A pattern that looks like it means "a .txt file directly inside temp/" actually means "a .txt file anywhere under a path containing temp/ at the start," because fnmatch's * has no special handling for /.
2. Why: look at the compiled regex
print(fnmatch.translate("temp/*.txt"))
(?s:temp/.*\.txt)\Z
Measured: fnmatch compiles * straight to regex .* under (?s:...) (re.DOTALL), so it matches any character at all, slashes and newlines included. There is no step that treats / as a boundary the way a shell glob or fnmatch.fnmatchcase's sibling glob module does.
3. glob.glob() on the identical pattern stops at the slash
import glob
print(glob.glob("sub/*.txt", root_dir="/tmp/globtest"))
['sub/normal.txt']
Measured: glob.glob() walks the pattern one path component at a time, so * only ever matches within a single directory level. Code that assumes fnmatch behaves the same way -- because both modules look like simple shell-style wildcard matchers -- will get silently broader matches than intended.
4. Fix: split on the separator yourself, or match basenames only
import fnmatch, posixpath
def matches_direct_child(path, dirname, filename_pattern):
d, f = posixpath.split(path)
return d == dirname and fnmatch.fnmatch(f, filename_pattern)
for c in candidates:
print(c, "->", matches_direct_child(c, "temp", "*.txt"))
temp/a.txt -> True
temp/sub/b.txt -> False
temp/sub/deep/d.txt -> False
Measured: splitting the directory from the filename and only running fnmatch against the filename component restores the "one level only" behaviour that the raw pattern looked like it should already have.
Frequently asked questions
Does fnmatch's * stop at a path separator like a shell glob?
No -- we measured fnmatch.fnmatch() matching a pattern like 'temp/*.txt' against paths several directories deeper than temp/, because its * has no special rule against matching '/'.
Does the glob module behave the same way as fnmatch?
No -- we confirmed glob.glob() walks a pattern one directory component at a time, so its * never crosses a path separator, unlike fnmatch.fnmatch()'s *.
Why does fnmatch's * cross slashes?
fnmatch.translate() compiles * directly to the regex .* under re.DOTALL -- we measured the exact compiled pattern, which has no logic distinguishing '/' from any other character.
Is fnmatch case-sensitive on Linux?
Yes by default -- we measured fnmatch.fnmatch('FOO.TXT', '*.txt') returning False on this Linux box, since os.path.normcase() is a no-op on POSIX; it would be case-insensitive on Windows.
How do I match only files directly inside a directory, not nested ones?
We confirmed splitting the path into its directory and filename parts with posixpath.split(), then running fnmatch only against the filename, restores the one-level-only matching a raw fnmatch pattern doesn't give you.
Written with AI assistance. Measured on 2026-10-04 with Python 3.13.5's built-in fnmatch and glob modules: compared fnmatch.fnmatch() and fnmatch.translate() against glob.glob() on patterns containing a literal path separator, applied to relative paths with multiple directory levels. PlainHeaders is our extension; Python's fnmatch and glob modules are built in.