Globbing in Python, Part 1
What globbing is
Globbing is pattern matching for file paths. Instead of listing every file by name, you describe a shape and the system finds the matches.
*.py → all Python files
data_?.csv → data_1.csv, data_A.csv, but not data_10.csv
[abc].txt → a.txt, b.txt, or c.txt
Python has a built-in glob module that does exactly this. No pip install needed.
The basics: * and ?
The two most common patterns:
*matches any number of characters (including zero)?matches exactly one character
import glob
# All Python files in the current directory
for f in glob.glob("*.py"):
print(f)
# Files like data_1.csv, data_X.csv — but not data_10.csv
for f in glob.glob("data_?.csv"):
print(f)
* is greedy. *.py matches setup.py, my_script.py, even .py (an empty name with an extension). If you want non-hidden files only, combine it:
# Hidden files (starting with .) are NOT matched by *
glob.glob("*.py") # finds app.py, not .hidden.py
Character classes: [...]
Square brackets match one character from a set:
# Exactly one character from the set a, b, or c
glob.glob("[abc].txt") # a.txt, b.txt, c.txt — not ab.txt
# Ranges work too
glob.glob("file[0-9].log") # file0.log, file1.log, ... file9.log
# Negation with ^ or !
glob.glob("file[^0-9].log") # fileX.log, fileA.log — not file1.log
Recursive glob with **
Python 3.5+ supports recursive globbing with **. This is the most useful pattern for real work:
import glob
# All Python files in current dir and every subdirectory
for f in glob.glob("**/*.py", recursive=True):
print(f)
# All markdown files anywhere under content/
for f in glob.glob("content/**/*.md", recursive=True):
print(f)
Without recursive=True, ** is treated like * — it does NOT descend into subdirectories.
glob vs os.scandir vs pathlib.Path.glob
Python gives you three ways to glob. Use each when:
| Method | Best for |
|---|---|
glob.glob() |
Quick one-liners, shell-like patterns |
pathlib.Path.glob() |
Object-oriented workflows, chaining with other path ops |
os.scandir() |
When you need fine control (filter by stat, avoid recursion) |
from pathlib import Path
# Same as glob.glob("**/*.py", recursive=True)
for f in Path(".").glob("**/*.py"):
print(f)
# Filter by something else
for f in Path(".").glob("**/*.py"):
if f.stat().st_size > 1000:
print(f"{f} ({f.stat().st_size} bytes)")
pathlib is the modern choice. It returns Path objects (not strings), so you can call .read_text(), .rename(), .stem, .suffix directly.
Common pitfalls
-
Hidden files are invisible to
*— files starting with.are skipped by default. If you need them, useglob.glob(".*")explicitly. -
**withoutrecursive=Trueis a no-op for recursion — it acts like*and only matches the current directory. -
Glob returns strings, not sorted — the order is filesystem-dependent. If you need sorted results, wrap it:
sorted(glob.glob("*.py")). -
No match returns an empty list, not an error — always handle the empty case if the result drives further logic.
files = glob.glob("nonexistent/*.py")
if not files:
print("No matches — check the directory and pattern.")
Try it yourself
Open a Python REPL and try:
import glob
glob.glob("**/*.md", recursive=True)
See how many markdown files exist under your current directory. Then try glob.glob("content/posts/**/*.md", recursive=True) to list every blog post.
In Part 2, we'll go further — fnmatch for string-only matching, brace expansion, writing a custom glob engine, and globbing across remote filesystems.