fnmatch Module Complexity¶
The fnmatch module provides Unix filename pattern matching using shell-style wildcards, useful for filtering filenames and simple pattern matching without regular expressions.
Complexity Reference¶
| Operation | Time | Space | Notes |
|---|---|---|---|
fnmatch() |
O(n) | O(1) | n = string length |
fnmatchcase() |
O(n) | O(1) | Case-sensitive version |
filter() |
O(k*n) | O(k) | k = items, n = string length |
translate() |
O(n) | O(n) | Convert pattern to regex |
Pattern Matching¶
Basic Patterns¶
import fnmatch
# Basic pattern matching - O(n)
print(fnmatch.fnmatch('hello.txt', '*.txt')) # True
print(fnmatch.fnmatch('test.py', '*.txt')) # False
print(fnmatch.fnmatch('file123.txt', 'file*.txt')) # True
# ? matches single character - O(n)
print(fnmatch.fnmatch('test1.txt', 'test?.txt')) # True
print(fnmatch.fnmatch('test12.txt', 'test?.txt')) # False
# [abc] matches any in brackets - O(n)
print(fnmatch.fnmatch('testa.txt', 'test[abc].txt')) # True
print(fnmatch.fnmatch('testd.txt', 'test[abc].txt')) # False
# [!abc] negation - O(n)
print(fnmatch.fnmatch('testd.txt', 'test[!abc].txt')) # True
print(fnmatch.fnmatch('testa.txt', 'test[!abc].txt')) # False
Pattern Wildcards¶
import fnmatch
# * - matches zero or more characters
print(fnmatch.fnmatch('file.txt', 'f*le.txt')) # True
# ? - matches exactly one character
print(fnmatch.fnmatch('file.txt', 'fil?.txt')) # True
# [seq] - matches any character in sequence
print(fnmatch.fnmatch('file1.txt', 'file[0-9].txt')) # True
# [!seq] - matches any character not in sequence
print(fnmatch.fnmatch('fileA.txt', 'file[!0-9].txt')) # True
Filtering Lists¶
Filter Filenames¶
import fnmatch
# Filter list of filenames - O(k*n)
filenames = ['test.py', 'data.csv', 'script.py', 'config.json', 'readme.txt']
# Find all Python files - O(k) items
py_files = fnmatch.filter(filenames, '*.py')
print(py_files) # ['test.py', 'script.py']
# Find files starting with 'test' - O(k)
test_files = fnmatch.filter(filenames, 'test*')
print(test_files) # ['test.py']
# Find config files - O(k)
configs = fnmatch.filter(filenames, 'config*')
print(configs) # ['config.json']
Case-Sensitive vs Insensitive¶
import fnmatch
import fnmatch
# Case-sensitive (default) - O(n)
print(fnmatch.fnmatch('Test.TXT', '*.txt')) # False
print(fnmatch.fnmatch('test.txt', '*.txt')) # True
# Case-insensitive version - O(n)
print(fnmatch.fnmatchcase('Test.TXT', '*.txt')) # False (always case-sensitive)
# Manual case-insensitive - O(n)
filename = 'Test.TXT'
pattern = '*.txt'
result = fnmatch.fnmatch(filename.lower(), pattern.lower())
print(result) # True
Advanced Usage¶
Translate to Regex¶
import fnmatch
import re
# Convert fnmatch pattern to regex - O(n)
pattern = fnmatch.translate('test*.py')
print(pattern) # Returns regex pattern
# Can use with re module - O(n)
regex = re.compile(pattern)
print(regex.match('test_script.py')) # Match object
print(regex.match('test.py')) # Match object
print(regex.match('other.py')) # None
Use Cases¶
File Filter Class¶
import fnmatch
import os
class FileFilter:
"""Filter files by pattern"""
def __init__(self, *patterns):
self.patterns = patterns
# Match filename - O(n) per pattern
def matches(self, filename):
for pattern in self.patterns: # O(m) patterns
if fnmatch.fnmatch(filename, pattern):
return True
return False
# Filter list - O(k*m*n)
def filter_files(self, filenames):
result = []
for filename in filenames: # O(k) files
if self.matches(filename):
result.append(filename)
return result
# Usage
filter = FileFilter('*.py', 'test_*.txt', 'data.*')
files = ['script.py', 'test_data.txt', 'readme.md', 'data.csv']
matching = filter.filter_files(files)
print(matching) # ['script.py', 'test_data.txt', 'data.csv']
Glob Alternative¶
import fnmatch
import os
def glob_alternative(directory, pattern):
"""Simulate glob using fnmatch - O(n)"""
result = []
# List all files - O(n)
for filename in os.listdir(directory):
# Check pattern - O(m) per file
if fnmatch.fnmatch(filename, pattern):
result.append(os.path.join(directory, filename))
return result
# Usage
py_files = glob_alternative('.', '*.py')
print(py_files)
Configuration Matcher¶
import fnmatch
class ConfigMatcher:
"""Match configuration entries"""
def __init__(self):
self.config = {
'app.name': 'MyApp',
'app.version': '1.0',
'db.host': 'localhost',
'db.port': '5432'
}
# Find config by pattern - O(k*n)
def find_configs(self, pattern):
result = {}
for key, value in self.config.items(): # O(k) items
if fnmatch.fnmatch(key, pattern): # O(n) per match
result[key] = value
return result
# Usage
config = ConfigMatcher()
app_configs = config.find_configs('app.*')
print(app_configs) # {'app.name': 'MyApp', 'app.version': '1.0'}
db_configs = config.find_configs('db.*')
print(db_configs) # {'db.host': 'localhost', 'db.port': '5432'}
Log Entry Filtering¶
import fnmatch
def filter_logs(log_entries, pattern):
"""Filter log entries by pattern - O(k*n)"""
matching = []
for entry in log_entries: # O(k) entries
# Match against pattern - O(n)
if fnmatch.fnmatch(entry, pattern):
matching.append(entry)
return matching
# Usage
logs = [
'ERROR: Database connection failed',
'INFO: Server started',
'ERROR: Authentication failed',
'DEBUG: Cache hit',
'WARNING: Low memory'
]
error_logs = filter_logs(logs, 'ERROR*')
print(error_logs)
Performance Characteristics¶
Time Complexity¶
- fnmatch(): O(n) where n = string length
- fnmatchcase(): O(n) case-sensitive matching
- filter(): O(k*n) where k = list size, n = string length
- translate(): O(n) to convert to regex
Space Complexity¶
- fnmatch(): O(1) - no extra space
- filter(): O(k) for result list
- translate(): O(n) for regex string
Benchmark¶
import fnmatch
import time
# Create test data
filenames = [f'file{i:04d}.txt' for i in range(1000)]
# Test fnmatch filter - O(k*n)
start = time.time()
for _ in range(100):
result = fnmatch.filter(filenames, 'file00*.txt')
fnmatch_time = time.time() - start
# Test list comprehension - O(k*n)
start = time.time()
for _ in range(100):
result = [f for f in filenames if fnmatch.fnmatch(f, 'file00*.txt')]
comp_time = time.time() - start
print(f"fnmatch.filter: {fnmatch_time:.4f}s")
print(f"List comprehension: {comp_time:.4f}s")
When to Use fnmatch¶
Good For¶
- Simple filename pattern matching
- Filtering lists of strings
- No need for complex regex
- Case-sensitive filename matching
- Shell-style pattern matching
Better Alternatives¶
# For more complex patterns, use regex
import re
result = re.match(r'test[0-9]{3}\.txt', filename)
# For filesystem globbing, use glob
import glob
files = glob.glob('*.txt')
# For case-insensitive matching on Windows
# Use glob with pathlib
from pathlib import Path
files = list(Path('.').glob('*.PY'))
# For complex filtering, use filter() or comprehensions
files = [f for f in filenames if 'test' in f and f.endswith('.py')]
Pattern Comparison¶
fnmatch vs glob vs re¶
import fnmatch
import glob
import re
filename = 'test_data_123.txt'
# fnmatch - simple shell patterns - O(n)
result = fnmatch.fnmatch(filename, 'test_*.txt')
# glob - filesystem patterns - O(n)
# glob is for filesystem, not just strings
# re - full regex - O(n)
result = re.match(r'test_.*\.txt', filename)
# fnmatch best for simple cases
Best Practices¶
Do's¶
- Use fnmatch for simple filename patterns
- Use filter() for batch matching
- Use fnmatchcase() when needed
- Translate to regex for complex patterns
Avoid's¶
- Don't use for complex regex needs
- Don't use for filesystem globbing (use glob)
- Don't assume case-insensitive on all systems
- Don't create regex for simple patterns
Common Patterns¶
Common Filter Patterns¶
import fnmatch
# Text files
text_files = fnmatch.filter(files, '*.txt')
# Python or JavaScript
code_files = fnmatch.filter(files, '*.py') + fnmatch.filter(files, '*.js')
# Test files
test_files = fnmatch.filter(files, 'test_*.py') + fnmatch.filter(files, '*_test.py')
# Backup files (to exclude)
backups = fnmatch.filter(files, '*.bak') + fnmatch.filter(files, '*~')
# Configuration files
configs = fnmatch.filter(files, '*.conf') + fnmatch.filter(files, '*.ini')