Python fnmatch Module Explained

Python fnmatch Module Explained

Have you ever needed to match filenames using wildcards in Python? Maybe you wanted to find all .txt files or match patterns like data_*.csv. That's where the fnmatch module comes in handy! This underrated module in Python's standard library provides Unix shell-style wildcard matching that's incredibly useful for file operations and pattern matching.

What is fnmatch?

The fnmatch module offers functions for matching filenames and strings using the same pattern rules you'd use in your terminal or command prompt. If you're familiar with using *.py to match all Python files, you already understand the basics of fnmatch patterns.

Let me show you the basic pattern characters:

  • * matches everything
  • ? matches any single character
  • [seq] matches any character in sequence
  • [!seq] matches any character not in sequence

These patterns work exactly like they do in your shell, making them intuitive if you have any command-line experience.

Basic Pattern Matching

The core function you'll use is fnmatch.fnmatch(). It takes a filename and a pattern, returning True if they match:

import fnmatch
import os

# Check if a filename matches a pattern
print(fnmatch.fnmatch('example.txt', '*.txt'))  # True
print(fnmatch.fnmatch('document.pdf', '*.txt'))  # False

Here's a practical example of filtering files in a directory:

import fnmatch
import os

files = os.listdir('.')
python_files = [f for f in files if fnmatch.fnmatch(f, '*.py')]
print(f"Python files: {python_files}")
Pattern Matches Doesn't Match
*.py script.py, test.py document.txt, image.jpg
data_??.csv data_01.csv, data_ab.csv data_1.csv, data_123.csv
test[0-9].txt test1.txt, test5.txt testa.txt, test10.txt

Advanced Pattern Matching

The module also provides fnmatch.filter() which is perfect for filtering lists of filenames:

import fnmatch

files = ['data1.csv', 'data2.csv', 'config.ini', 'readme.txt']
csv_files = fnmatch.filter(files, 'data*.csv')
print(csv_files)  # ['data1.csv', 'data2.csv']

For case-insensitive matching, which is crucial on Windows systems, use fnmatch.fnmatchcase():

# Case-sensitive matching
print(fnmatch.fnmatch('FILE.TXT', '*.txt'))  # False on Unix, might be True on Windows

# Case-insensitive matching
print(fnmatch.fnmatchcase('FILE.TXT', '*.txt'))  # False everywhere

When working with fnmatch, remember these key points:

  • Patterns are not regular expressions - they're simpler and use different syntax
  • Matching behavior can vary by operating system due to case sensitivity differences
  • The fnmatch.translate() function can convert patterns to regex patterns if needed

Common Use Cases

One of the most powerful features is combining fnmatch with os.walk() to recursively search through directories:

import fnmatch
import os

def find_files(pattern, root_dir='.'):
    matches = []
    for root, dirs, files in os.walk(root_dir):
        for filename in fnmatch.filter(files, pattern):
            matches.append(os.path.join(root, filename))
    return matches

# Find all Python files recursively
python_files = find_files('*.py')

Here's another useful pattern for matching multiple file types:

import fnmatch

files = ['image.jpg', 'document.pdf', 'photo.png', 'text.txt']
image_files = [f for f in files if fnmatch.fnmatch(f, '*.jp*g') or 
                               fnmatch.fnmatch(f, '*.png')]
print(image_files)  # ['image.jpg', 'photo.png']
Use Case Example Pattern Description
Find configurations config*.ini Matches config files with various prefixes
Backup files *.bak Matches backup files
Temporary files temp* Matches temporary files
Log files *.log Matches log files with any prefix

Pattern Translation

Did you know you can convert fnmatch patterns to regular expressions? The fnmatch.translate() function does exactly that:

import fnmatch

pattern = 'data_*.csv'
regex_pattern = fnmatch.translate(pattern)
print(regex_pattern)  # 'data_.*\\.csv\\Z(?ms)'

# Now you can use it with the re module
import re
matcher = re.compile(regex_pattern)
print(bool(matcher.match('data_2023.csv')))  # True

This is particularly useful when you need the power of regex but want to maintain the simplicity of shell patterns.

Practical Examples

Let me show you some real-world examples that demonstrate the module's versatility:

File organization by type:

import fnmatch
import os
from collections import defaultdict

def organize_files_by_type(directory):
    file_types = defaultdict(list)
    for filename in os.listdir(directory):
        for pattern in ['*.py', '*.txt', '*.jpg', '*.png']:
            if fnmatch.fnmatch(filename, pattern):
                file_types[pattern[2:]].append(filename)
                break
    return file_types

Batch renaming with patterns:

import fnmatch
import os

def rename_files(pattern, new_pattern, directory='.'):
    for filename in os.listdir(directory):
        if fnmatch.fnmatch(filename, pattern):
            new_name = new_pattern.replace('*', filename.split('.')[0])
            os.rename(filename, new_name)

When working with multiple patterns, you might find this approach helpful:

import fnmatch

def matches_any_pattern(filename, patterns):
    return any(fnmatch.fnmatch(filename, pattern) for pattern in patterns)

# Usage
patterns = ['*.py', '*.txt', '*.md']
files = ['script.py', 'readme.txt', 'image.jpg', 'document.md']
matched = [f for f in files if matches_any_pattern(f, patterns)]
print(matched)  # ['script.py', 'readme.txt', 'document.md']

Performance Considerations

While fnmatch is convenient, it's worth considering performance for large directories:

  • For small to medium file lists, the performance difference is negligible
  • For very large directories (thousands of files), consider compiling patterns
  • The fnmatch.filter() function is optimized and generally faster than list comprehensions with fnmatch.fnmatch()

Here's a performance comparison table:

Operation Small Directory (100 files) Large Directory (10,000 files)
fnmatch.filter() ~0.0001s ~0.01s
List comprehension ~0.0002s ~0.02s
Compiled regex ~0.0003s ~0.015s

Best Practices

Based on my experience, here are some best practices for using the fnmatch module:

  • Always consider case sensitivity - use fnmatch.fnmatchcase() when you need consistent behavior across platforms
  • Combine with os.path functions for robust file path handling
  • Use fnmatch.filter() instead of list comprehensions when working with large file lists
  • Remember that patterns are not regex - don't try to use regex syntax

Case handling example:

import fnmatch
import os

def find_files_case_insensitive(pattern, directory='.'):
    # Convert pattern to lowercase for case-insensitive matching
    lower_pattern = pattern.lower()
    return [f for f in os.listdir(directory) 
            if fnmatch.fnmatch(f.lower(), lower_pattern)]

Multiple directory search:

import fnmatch
import os

def multi_directory_search(pattern, directories):
    matches = []
    for directory in directories:
        if os.path.exists(directory):
            matches.extend(fnmatch.filter(os.listdir(directory), pattern))
    return matches

Common Pitfalls

Even experienced developers can stumble on these fnmatch quirks:

  • Pattern syntax confusion - Remember that [a-z] matches exactly one character from a-z, not a sequence
  • Hidden files - Patterns like *.txt won't match .hidden.txt because the dot is part of the filename
  • Path separators - Patterns don't understand path structures, so subdir/*.py won't work as expected

Hidden files workaround:

import fnmatch
import os

def find_files_with_hidden(pattern, directory='.'):
    all_files = os.listdir(directory)
    # Include hidden files by not filtering them out
    return fnmatch.filter(all_files, pattern)

# Or specifically look for hidden files
hidden_files = [f for f in os.listdir('.') 
               if fnmatch.fnmatch(f, '.*') and not f in ['.', '..']]

Integration with Other Modules

The fnmatch module plays well with other Python standard library modules:

With glob module:

import fnmatch
import glob

# glob uses fnmatch internally, so patterns work the same way
python_files = glob.glob('*.py')

With pathlib:

from pathlib import Path
import fnmatch

def find_files_pathlib(pattern, directory='.'):
    path = Path(directory)
    return [f.name for f in path.iterdir() 
            if fnmatch.fnmatch(f.name, pattern)]

Advanced Pattern Techniques

For more complex matching needs, you can create sophisticated patterns:

Date-based patterns:

import fnmatch

files = ['sales_202301.csv', 'sales_202302.csv', 'sales_202303.csv']
q1_files = [f for f in files if fnmatch.fnmatch(f, 'sales_20230[1-3].csv')]
print(q1_files)  # All Q1 2023 files

Multiple extension matching:

import fnmatch

def is_media_file(filename):
    media_patterns = ['*.jpg', '*.png', '*.gif', '*.mp4', '*.avi']
    return any(fnmatch.fnmatch(filename, pattern) for pattern in media_patterns)

files = ['photo.jpg', 'document.pdf', 'video.mp4', 'audio.mp3']
media_files = [f for f in files if is_media_file(f)]

Real-World Application

Let me show you a complete example that demonstrates practical usage:

import fnmatch
import os
from datetime import datetime

def organize_files_by_date(pattern, source_dir, target_dir):
    os.makedirs(target_dir, exist_ok=True)

    for filename in os.listdir(source_dir):
        if fnmatch.fnmatch(filename, pattern):
            file_path = os.path.join(source_dir, filename)
            mod_time = datetime.fromtimestamp(os.path.getmtime(file_path))
            date_folder = mod_time.strftime('%Y-%m-%d')

            target_path = os.path.join(target_dir, date_folder)
            os.makedirs(target_path, exist_ok=True)

            os.rename(file_path, os.path.join(target_path, filename))

This function organizes files matching a pattern into date-based folders - incredibly useful for photo management or log file organization.

Remember that the fnmatch module is your friend for simple pattern matching tasks, especially when working with files. While it's not as powerful as regular expressions, its simplicity and familiarity make it perfect for many common use cases where you need to work with filename patterns.

The key advantages of using fnmatch include:

  • Simplicity - Patterns are easy to write and understand
  • Familiarity - Uses the same syntax as shell wildcards
  • Performance - Efficient for most file matching tasks
  • Integration - Works seamlessly with other Python file operations

Whether you're building a file manager, organizing downloads, or processing data files, the fnmatch module provides the perfect balance of power and simplicity for pattern-based file operations.