fnmatch Bracket Caret Is Literal, Not Negation (tested)
In regular expressions, [^t] inside brackets means "not t". We measured what Python's fnmatch module does with that exact syntax, and it means something else entirely.
1. A regex-style negated bracket silently matches the thing it was meant to exclude
import fnmatch
files = ["tmp_a.txt", "real_a.txt", "^weird.txt", "tmp_b.txt"]
pattern = "[^t]*.txt" # intended: "does NOT start with t"
print(fnmatch.filter(files, pattern))
['tmp_a.txt', '^weird.txt', 'tmp_b.txt']
Measured: the pattern was meant to keep everything except files starting with t, but it kept exactly the two files starting with t plus a literal edge case -- the opposite of the intent -- with no exception or warning.
2. Why: fnmatch brackets only treat '!' as negation; '^' is just a literal character
import fnmatch
print(fnmatch.translate("[^t]*.txt"))
print(fnmatch.translate("[!t]*.txt"))
(?s:[\^t].*\.txt)\Z
(?s:[^t].*\.txt)\Z
Measured: fnmatch.translate() shows exactly what happens -- [^t] compiles to the regex char class [\^t], matching a literal ^ or t. Only [!t] (fnmatch's own negation syntax) compiles to the real negated regex class [^t]. The ^ you typed never becomes a negation operator; it becomes one more character to match.
3. Fix: use '!' for negation in fnmatch/glob brackets, never '^'
import fnmatch
files = ["tmp_a.txt", "real_a.txt", "^weird.txt", "tmp_b.txt"]
print(fnmatch.filter(files, "[!t]*.txt"))
['real_a.txt', '^weird.txt']
Measured: swapping ^ for ! gives the correct result -- every file not starting with t, including the literal ^weird.txt which was never a t-file to begin with. This same rule applies to glob.glob() and pathlib.Path.glob(), since both build their matching on fnmatch.translate() internally.
Frequently asked questions
Does [^x] mean 'not x' in Python's fnmatch module?
No -- we measured it compiling to a literal character class matching '^' or 'x', not a negated class. fnmatch's negation syntax is '[!x]', not '[^x]'.
Does fnmatch raise an error if I use ^ for negation by mistake?
No -- we saw no exception or warning. The pattern compiles and runs normally, it just silently matches the wrong set of files.
Does this affect glob.glob() and pathlib too?
Yes -- both glob.glob() and pathlib.Path.glob() build their pattern matching on fnmatch.translate() internally, so the same '[^x] is literal, not negation' rule applies to them as well.
How can I see exactly what a bracket pattern will match?
Call fnmatch.translate(pattern) -- we used it above to show the exact regex a pattern compiles to, which immediately revealed the caret was being treated literally.
What is the correct way to write a negated character class in fnmatch?
Use '!' as the first character inside the brackets, e.g. '[!t]' to mean 'not t'. We confirmed this compiles to the real negated regex class [^t].
Written with AI assistance. Measured on 2026-10-04 with Python 3.13.5's built-in fnmatch module: ran fnmatch.filter() with a bracket pattern using regex-style '^' negation, then compared against fnmatch's actual negation syntax '!'. PlainHeaders is our extension; Python's fnmatch module is built in.