The fnmatch module matches filenames against shell-style wildcards. Unlike glob, it doesn't touch the filesystem—just pattern matching.

Basic Matching

import fnmatch
 
# Does filename match pattern?
fnmatch.fnmatch('data.txt', '*.txt')      # True
fnmatch.fnmatch('data.txt', 'data.*')     # True
fnmatch.fnmatch('data.txt', '*.py')       # False
fnmatch.fnmatch('readme.md', 'READ*')     # True (case-insensitive on Windows)

Case Sensitivity

import fnmatch
 
# fnmatch: follows OS convention
# Case-insensitive on Windows, sensitive on Unix
fnmatch.fnmatch('DATA.txt', 'data.*')  # True on Windows, False on Unix
 
# fnmatchcase: always case-sensitive
fnmatch.fnmatchcase('DATA.txt', 'data.*')  # False
fnmatch.fnmatchcase('data.txt', 'data.*')  # True

Pattern Syntax

import fnmatch
 
# * matches everything
fnmatch.fnmatch('anything.txt', '*')       # True
fnmatch.fnmatch('file.txt', '*.txt')       # True
 
# ? matches single character
fnmatch.fnmatch('file1.txt', 'file?.txt')  # True
fnmatch.fnmatch('file10.txt', 'file?.txt') # False
 
# [seq] matches characters in seq
fnmatch.fnmatch('file1.txt', 'file[123].txt')  # True
fnmatch.fnmatch('file4.txt', 'file[123].txt')  # False
 
# [!seq] matches characters NOT in seq
fnmatch.fnmatch('fileA.txt', 'file[!0-9].txt') # True
fnmatch.fnmatch('file1.txt', 'file[!0-9].txt') # False

Filtering Lists

import fnmatch
 
files = ['data.csv', 'report.csv', 'image.png', 'notes.txt', 'backup.csv']
 
# Filter matching files
csv_files = fnmatch.filter(files, '*.csv')
# ['data.csv', 'report.csv', 'backup.csv']
 
# Filter with character class
r_files = fnmatch.filter(files, '[rn]*')
# ['report.csv', 'notes.txt']

Translate to Regex

import fnmatch
import re
 
# Convert pattern to regex
regex = fnmatch.translate('*.txt')
print(regex)  # '(?s:.*\\.txt)\\Z'
 
# Use compiled regex
pattern = re.compile(fnmatch.translate('*.txt'))
pattern.match('file.txt')   # Match object
pattern.match('file.csv')   # None
 
# Useful for complex matching
def multi_match(filename, patterns):
    """Check if filename matches any pattern."""
    combined = '|'.join(fnmatch.translate(p) for p in patterns)
    return re.match(combined, filename) is not None
 
multi_match('test.py', ['*.py', '*.txt'])  # True

Practical Examples

Filtering Directory Listings

import fnmatch
import os
 
def find_files(directory, pattern):
    """Find files matching pattern in directory."""
    matches = []
    for filename in os.listdir(directory):
        if fnmatch.fnmatch(filename, pattern):
            matches.append(os.path.join(directory, filename))
    return matches
 
python_files = find_files('.', '*.py')

.gitignore-Style Matching

import fnmatch
 
def is_ignored(filepath, patterns):
    """Check if path matches any ignore pattern."""
    for pattern in patterns:
        if fnmatch.fnmatch(filepath, pattern):
            return True
        # Also check basename
        if fnmatch.fnmatch(os.path.basename(filepath), pattern):
            return True
    return False
 
ignore_patterns = ['*.pyc', '__pycache__', '.git', '*.log']
is_ignored('cache/__pycache__', ignore_patterns)  # True
is_ignored('main.py', ignore_patterns)             # False

File Type Categorization

import fnmatch
 
FILE_TYPES = {
    'images': ['*.jpg', '*.jpeg', '*.png', '*.gif', '*.svg'],
    'documents': ['*.pdf', '*.doc', '*.docx', '*.txt', '*.md'],
    'code': ['*.py', '*.js', '*.ts', '*.go', '*.rs'],
    'data': ['*.json', '*.csv', '*.xml', '*.yaml'],
}
 
def categorize_file(filename):
    """Return category for a filename."""
    for category, patterns in FILE_TYPES.items():
        for pattern in patterns:
            if fnmatch.fnmatch(filename.lower(), pattern):
                return category
    return 'other'
 
categorize_file('photo.jpg')   # 'images'
categorize_file('main.py')     # 'code'
categorize_file('data.csv')    # 'data'

Exclude Filter

import fnmatch
 
def filter_exclude(names, include, exclude=None):
    """Filter names: include matching, exclude specified."""
    result = fnmatch.filter(names, include)
    if exclude:
        result = [n for n in result if not fnmatch.fnmatch(n, exclude)]
    return result
 
files = ['test_main.py', 'main.py', 'test_utils.py', 'utils.py']
filter_exclude(files, '*.py', 'test_*')
# ['main.py', 'utils.py']

fnmatch vs glob vs re

Featurefnmatchglobre
Filesystem accessNoYesNo
Pattern syntaxShellShellRegex
ComplexitySimpleSimpleComplex
Use caseFilter stringsFind filesComplex patterns
# fnmatch: match strings against patterns
fnmatch.fnmatch('file.txt', '*.txt')
 
# glob: find actual files
import glob
glob.glob('*.txt')
 
# re: complex pattern matching
import re
re.match(r'.*\.txt$', 'file.txt')

Quick Reference

import fnmatch
 
# Match single filename
fnmatch.fnmatch(name, pattern)       # OS case rules
fnmatch.fnmatchcase(name, pattern)   # Case-sensitive
 
# Filter list
fnmatch.filter(names, pattern)
 
# Convert to regex
fnmatch.translate(pattern)
PatternMatches
*Everything
?Single character
[seq]Any char in seq
[!seq]Any char not in seq
[a-z]Range

fnmatch is pure pattern matching without filesystem access. Use it when you have filenames as strings and need to filter them.

React to this post: