Python Wildcard Pattern Matching with fnmatch and glob
python wildcard pattern: Learn how to use Python's wildcard pattern matching with fnmatch, glob, and pathlib to filter strings and files efficiently.
python wildcard pattern requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When you need to match strings against simple patterns like *.txt or data_?.csv, Python provides built-in tools that avoid the overhead of regular expressions. The fnmatch module handles string matching, while glob and pathlib extend the same wildcard syntax to file paths. Understanding how these tools interpret *, ?, and character sequences lets you write concise filters without pulling in a regex engine.
Matching Strings with fnmatch
The fnmatch module translates a wildcard pattern into a regular expression internally and matches it against a string. The core function is fnmatch.fnmatch(name, pattern), which returns True when name matches pattern. The pattern uses * to match any sequence of characters, ? to match a single character, and [seq] to match any character in the sequence. For example:
import fnmatch print(fnmatch.fnmatch("report_final.txt", "report_*.txt")) # True print(fnmatch.fnmatch("data_1.csv", "data_?.csv")) # True print(fnmatch.fnmatch("data_10.csv", "data_?.csv")) # False print(fnmatch.fnmatch("log.txt", "log[0-9].txt")) # False
The [seq] syntax supports ranges like [a-z] and negation with [!seq]. On Windows, fnmatch normalizes case because the underlying OS treats filenames case-insensitively. On other platforms, matching is case-sensitive by default. If you need consistent behavior across operating systems, use fnmatch.fnmatchcase instead, which always performs case-sensitive matching.
fnmatch also provides fnmatch.filter(names, pattern) to filter an iterable of strings in one call:
files = ["a.py", "b.py", "c.txt", "d.py"] py_files = fnmatch.filter(files, "*.py") print(py_files) # ['a.py', 'b.py', 'd.py']
This is more readable than a list comprehension with a lambda and is implemented in C for speed.
Matching File Paths with glob
While fnmatch works on plain strings, glob applies wildcard patterns to filesystem paths. The glob.glob(pathname) function returns a list of paths that match the pattern, following the same wildcard rules as fnmatch but with path separators handled specially. A * does not cross directory boundaries, so *.txt only matches files in the current directory, not in subdirectories. To match recursively, use ** with recursive=True:
import glob # All .py files in current directory print(glob.glob("*.py")) # All .py files in current directory and subdirectories print(glob.glob("**/*.py", recursive=True))
glob also supports the ? and [seq] patterns. The returned paths are not sorted by default, so you may want to sort them yourself. If no matches are found, an empty list is returned.
A common mistake is assuming that glob returns absolute paths. It returns paths exactly as constructed from the pattern, so relative patterns yield relative paths. Use os.path.abspath or pathlib.Path.resolve() if you need absolute paths.
Using pathlib for Wildcard Paths
The pathlib module offers an object-oriented interface for filesystem paths and includes wildcard support through the Path.glob() method. This method returns a generator of Path objects that match the given pattern. The pattern syntax is identical to glob, including ** for recursive matching:
from pathlib import Path for path in Path(".").glob("*.py"): print(path) for path in Path(".").glob("**/*.txt"): print(path)
Using Path.glob() is often more convenient than glob.glob() because you can chain path operations directly:
configs = [p for p in Path("config").glob("*.json") if p.stat().st_size > 1000]
pathlib also provides Path.rglob() for recursive matching, which is equivalent to glob("**/*") but with the pattern applied to the full path. This is useful for finding files with a specific extension in a directory tree.
Combining Wildcards with Regular Expressions
The wildcard syntax is intentionally limited. It cannot express patterns like "matches a date in YYYY-MM-DD format" or "does not contain a digit". When you need logical conditions on how many times a character can repeat or require specific character classes, you should switch to re module. A common approach is to convert a wildcard pattern to a regular expression manually:
import re def wildcard_to_regex(pattern): regex = "" i = 0 while i < len(pattern): c = pattern[i] if c == '*': regex += '.*' elif c == '?': regex += '.' elif c == '[': j = i + 1 if j < len(pattern) and pattern[j] == '!': j += 1 regex += '[^' else: regex += '[' while j < len(pattern) and pattern[j] != ']': regex += pattern[j] j += 1 if j < len(pattern): regex += ']' i = j else: # Unterminated bracket: treat literally regex += '\\[' else: regex += re.escape(c) i += 1 return regex
This function is simplistic and does not handle all edge cases, but it shows the conversion principle. In practice, fnmatch already does this conversion internally, so you rarely need to write your own. Use re only when the pattern needs lookaheads, alternation, or quantifiers that wildcards cannot express.
Performance and Runtime Considerations
For simple patterns, fnmatch and glob are fast because they compile the pattern once per call. However, if you call fnmatch in a tight loop over thousands of strings, the repeated compilation overhead can become noticeable. In that case, pre-compile the pattern using fnmatch.translate() and re.compile():
import fnmatch import re pattern = re.compile(fnmatch.translate("*.txt")) for name in many_names: if pattern.match(name): # process
glob performs filesystem I/O, so its performance depends on the number of files and directories scanned. Using ** recursively can be expensive on large trees. If you only need a single level, avoid ** and use * instead.
Another consideration is memory. glob.glob() returns a list, which can be large if many files match. pathlib.Path.glob() returns a generator, allowing you to process matches lazily and avoid holding all results in memory.
Common Mistakes and Edge Cases
One common mistake is forgetting that * does not match hidden files (those starting with a dot) unless explicitly included. For example, glob.glob("*.txt") will not match .hidden.txt. To include hidden files, use a pattern like .*.txt or * with include_hidden=True (available in Python 3.11+ for pathlib).
Another edge case is the behavior of ? on Windows. Since ? matches any single character, it also matches the path separator on some systems? Actually, in fnmatch, ? does not match a slash on Unix-like systems because the pattern is matched against the whole string. In glob, ? does not cross directory boundaries, so ? will not match a /. This is consistent with *.
Bracket expressions can be tricky. [!a-z] matches any character not in the range, but the ! must be the first character after [. Also, to include a literal ] in a bracket expression, place it first or escape it. fnmatch does not support escaping with backslashes; instead, you can use bracket notation to match a literal * or ? (e.g., [*] matches a star).
Choosing the Right Wildcard Tool
Use fnmatch when you need to match strings that are not necessarily file paths, such as filtering log messages or validating user input against a simple pattern. Use glob when you need to find files on disk and you want the results as strings. Use pathlib.Path.glob() when you are already working with Path objects and want to chain operations or need a generator to handle large directory trees.
For patterns that require more expressive power than wildcards provide, switch to regular expressions. The decision is not about which is "better" but about matching the tool to the complexity of the pattern and the data source. Wildcards are ideal for quick, human-readable filters; regex is necessary when the pattern has logical conditions that wildcards cannot encode.