Finding Files with glob & fnmatch
Reviewed & published by Brayan K
The glob module finds real files on disk that match a wildcard pattern, while fnmatch tests whether a filename string matches such a pattern — together they make file searching simple and readable.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Instead of manually walking directories, you write a pattern like *.txt or **/*.py and Python hands you the matching paths. It's one of the most practical everyday skills in the standard library.
What You'll Learn in This Lesson
- Matching files with *, ?, and [ ] wildcards
- Searching subfolders with recursive ** patterns
- Using Path.glob and Path.rglob from pathlib
- Filtering name strings with fnmatch
- Case-sensitive matching with fnmatchcase
- Sorting results for deterministic output
1 glob.glob with *, ?, and [ ]
We create a few files in a temp directory so the demo is deterministic, then match them. * matches any run of characters, ? matches exactly one. Always sorted() the results for a stable order:
import tempfile, os, glob
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
for name in ["a.txt", "b.txt", "c.csv", "notes.md", "data1.txt"]:
Path(d, name).touch()
txts = sorted(glob.glob(os.path.join(d, "*.txt")))
print("TXT files:", [os.path.basename(p) for p in txts])
# ['a.txt', 'b.txt', 'data1.txt']
one_char = sorted(glob.glob(os.path.join(d, "?.txt")))
print("Single-char names:", [os.path.basename(p) for p in one_char])
# ['a.txt', 'b.txt']import tempfile, os, glob
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
for name in ["a.txt", "b.txt", "c.csv", "notes.md", "data1.txt"]:
Path(d, name).touch()
txts = sorted(glob.glob(os.path.join(d, "*.txt")))
print("TXT files:", [os.path.basename(p) for p in txts])
one_char = sorted(glob.glob(os.path.join(d, "?.txt")))
print("Single-char names:", [os.path.basename(p) for p in one_char])
# Expected output:
# TXT files: ['a.txt', 'b.txt', 'data1.txt']
# Single-char names: ['a.txt', 'b.txt']2 Recursive ** and Path.glob
To search inside subdirectories, use ** with recursive=True. Even nicer is pathlib: Path.glob for one level, Path.rglob for a full recursive walk — and it yields Path objects with a handy .name:
import tempfile, glob, os
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
Path(d, "sub").mkdir()
Path(d, "top.py").touch()
Path(d, "sub", "deep.py").touch()
Path(d, "sub", "readme.txt").touch()
# Recursive search across all subfolders
py_files = sorted(glob.glob(os.path.join(d, "**", "*.py"), recursive=True))
print("Python files found:", [os.path.basename(p) for p in py_files])
# ['deep.py', 'top.py']import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
base = Path(d)
for name in ["log1.log", "log2.log", "main.py", "test.py"]:
(base / name).touch()
logs = sorted(p.name for p in base.glob("*.log"))
print("Logs:", logs) # ['log1.log', 'log2.log']
py = sorted(p.name for p in base.glob("*.py"))
print("Python:", py) # ['main.py', 'test.py']import tempfile, glob, os
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
Path(d, "sub").mkdir()
Path(d, "top.py").touch()
Path(d, "sub", "deep.py").touch()
Path(d, "sub", "readme.txt").touch()
py_files = sorted(glob.glob(os.path.join(d, "**", "*.py"), recursive=True))
print("Python files found:", [os.path.basename(p) for p in py_files])
# Expected output:
# Python files found: ['deep.py', 'top.py']3 fnmatch: Matching Names Without Touching Disk
fnmatch tests a string against a glob-style pattern — no filesystem access. Perfect for filtering a list of names you already have. fnmatchcase is always case-sensitive, regardless of operating system:
import fnmatch
names = ["report_jan.csv", "report_feb.csv", "summary.txt", "report_mar.xlsx"]
matches = [n for n in names if fnmatch.fnmatch(n, "report_*.csv")]
print("CSV reports:", matches)
# ['report_jan.csv', 'report_feb.csv']
# fnmatchcase is ALWAYS case-sensitive
print(fnmatch.fnmatchcase("Photo.JPG", "*.JPG")) # True
print(fnmatch.fnmatchcase("Photo.JPG", "*.jpg")) # Falseimport fnmatch
names = ["report_jan.csv", "report_feb.csv", "summary.txt", "report_mar.xlsx"]
matches = [n for n in names if fnmatch.fnmatch(n, "report_*.csv")]
print("CSV reports:", matches)
print(fnmatch.fnmatchcase("Photo.JPG", "*.JPG"))
print(fnmatch.fnmatchcase("Photo.JPG", "*.jpg"))
# ✅ Expected output:
# CSV reports: ['report_jan.csv', 'report_feb.csv']
# True
# False🎯 Your Turn: Fill in the Blanks
Replace each ___ so the code finds all PNG files in the temp directory and counts them.
import tempfile, glob, os
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
for name in ["img1.png", "img2.png", "doc.pdf"]:
Path(d, name).touch()
pngs = sorted(glob.glob(os.path.join(d, "*.___"))) # hint: the extension
print("PNG count:", ___(pngs)) # hint: count items
print([os.path.basename(p) for p in pngs])
# Expected output:
# PNG count: 2
# ['img1.png', 'img2.png']⚠️ Common Errors
glob.glob("dir/**/*.py") # ** behaves like a single *glob.glob("dir/**/*.py", recursive=True)❌ Assuming results are sorted
for path in glob.glob("*.txt"): # arbitrary order
print(path)for path in sorted(glob.glob("*.txt")):
print(path)❌ Expecting glob to match hidden dotfiles
A leading * does not match files starting with a dot (like .env). To include them, match the dot explicitly with a pattern like .*.
🎯 Mini Challenge: Find & Filter Logs
Recursively find every .log file with rglob, then use fnmatch to keep only the ones whose name starts with app.
import tempfile, fnmatch
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
base = Path(d)
(base / "logs").mkdir()
for f in ["app.log", "error.log", "app.txt", "logs/db.log", "logs/note.md"]:
(base / f).touch()
all_logs = sorted(p.name for p in base.rglob("*.log"))
print("All .log files:", all_logs)
app_only = [n for n in all_logs if fnmatch.fnmatch(n, "app*")]
print("Matching 'app*':", app_only)
# Expected output:
# All .log files: ['app.log', 'db.log', 'error.log']
# Matching 'app*': ['app.log']📚 Keep Learning
- • glob — Unix style pathname pattern expansion
- • fnmatch — Unix filename pattern matching
- • pathlib.Path.glob & rglob
Frequently Asked Questions
Lesson complete — you can find any file by pattern!
You can match files with *, ?, and [ ], search subfolders with recursive ** and rglob, filter name strings with fnmatch, and you always sort() for deterministic results.
Practice quiz
In a glob pattern, what does * match?
- Exactly one character
- Only digits
- Any number of characters (including none) within a path segment
- Only the dot in a filename
Answer: Any number of characters (including none) within a path segment. * matches any run of characters within a single path segment; ? matches exactly one character.
What does ? match in a glob pattern?
- Exactly one character
- Any number of characters
- A literal question mark
- A directory boundary
Answer: Exactly one character. ? matches exactly one character, so ?.txt matches a.txt and b.txt but not data1.txt.
How do you search inside subfolders with glob.glob?
- Use * at the end
- Use fnmatch instead
- Use a trailing slash
- Use ** with recursive=True
Answer: Use ** with recursive=True. Recursive search needs the ** wildcard together with recursive=True.
Without recursive=True, how does ** behave in glob.glob?
- It raises an error
- It behaves like a single *
- It matches hidden files
- It matches nothing
Answer: It behaves like a single *. ** only crosses folder boundaries when recursive=True; otherwise it acts like a single *.
When would you use fnmatch instead of glob?
- When you already have a list of name strings to filter, with no disk access
- When you want to touch the filesystem
- When you need to create files
- When matching binary data
Answer: When you already have a list of name strings to filter, with no disk access. fnmatch just tests strings against a pattern without filesystem access — ideal for filtering an existing list.
What does fnmatch.fnmatchcase("Photo.JPG", "*.jpg") return?
- True
- An error
- False
- None
Answer: False. fnmatchcase is always case-sensitive, so uppercase .JPG does not match lowercase *.jpg — it returns False.
What does fnmatch.fnmatchcase("Photo.JPG", "*.JPG") return?
- False
- True
- An error
- None
Answer: True. Case matches exactly here, so fnmatchcase returns True.
In what order does glob.glob return its results?
- Always alphabetical
- Reverse alphabetical
- By file size
- Arbitrary, filesystem-dependent order
Answer: Arbitrary, filesystem-dependent order. glob returns paths in arbitrary order; wrap it in sorted() for deterministic output.
How is fnmatch.fnmatchcase different from plain fnmatch.fnmatch?
- fnmatchcase ignores case
- fnmatchcase is always case-sensitive regardless of OS
- fnmatchcase touches the disk
- There is no difference
Answer: fnmatchcase is always case-sensitive regardless of OS. Plain fnmatch follows OS case rules; fnmatchcase is always case-sensitive everywhere.
Does a leading * match hidden dotfiles like .env?
- Yes, always
- Only with recursive=True
- No — you must match the dot explicitly
- Only on Windows
Answer: No — you must match the dot explicitly. A leading * does not match files starting with a dot; match the dot explicitly (e.g. .*) to include them.
Continue this course
- Previous: Temporary Files & Directories
- Next: High-Level File Operations (shutil) — Copy, move, archive and delete trees with the shutil module
- Quick reference: Python cheat sheet
Frequently asked questions
What do *, ?, and [ ] mean in a glob pattern?
An asterisk * matches any number of characters (including none) within a single path segment. A question mark ? matches exactly one character. Square brackets [ ] match any one character from a set, like [abc] or a range like [0-9]. They make pattern matching read almost like English: *.txt means 'anything ending in .txt'.
How do I search inside subfolders?
Use the ** wildcard with recursive=True: glob.glob('dir/**/*.py', recursive=True) walks every subdirectory. With pathlib it's even cleaner: Path('dir').rglob('*.py') does the same recursive search. The single * only matches within one folder level; ** crosses folder boundaries.
When would I use fnmatch instead of glob?
glob touches the filesystem to find real files. fnmatch just tests whether a name string matches a pattern — no disk access. Use fnmatch when you already have a list of names (from an API, a database, or a directory listing) and want to filter them, or to test a single filename against a pattern.
Why aren't my glob results in alphabetical order?
glob returns paths in arbitrary, filesystem-dependent order — not sorted. If you need a predictable order (for stable output, reproducible tests, or display), wrap the result in sorted(). Relying on the raw order is a common source of flaky, non-deterministic behavior.