pathspec
pathspec answers one question well: does this file path match this set of gitignore-style patterns? You compile pattern lines (usually straight out of a .gitignore) into a PathSpec object, then test single paths, filter iterables of paths, or walk a directory tree. It ships two implementations: PathSpec with the 'gitignore' pattern type follows the gitignore documentation literally, while GitIgnoreSpec replicates what Git actually does, including the undocumented edge cases like negations re-including files from excluded directories. It is pure Python with zero required dependencies, and its enormous download count comes from being the ignore engine inside tools like Black and MkDocs.
The standard way to do gitignore matching in Python, proven inside Black and MkDocs; use GitIgnoreSpec when fidelity to Git matters. Skip it when stdlib fnmatch would do, and read the 1.0 upgrade notes before bumping from 0.x.
Use it if
- Your tool needs to respect .gitignore files the way Git does: GitIgnoreSpec exists precisely for that and matches Git's edge cases, not just the docs
- You let users configure include/exclude rules and want them to write familiar gitignore syntax instead of raw regex or fnmatch globs
- You need to filter thousands of paths and can install an optional speedup: the backend parameter switches matching to google-re2 or hyperscan for large pattern sets
- You want zero required dependencies in a library of your own, since pathspec adds nothing else to your dependency tree
- You just need simple glob matching on a few paths: fnmatch and pathlib.PurePath.match are in the standard library and cost nothing
- You migrated from 0.x without reading the upgrade guide: 1.0 (January 2026) renamed 'gitwildmatch' to 'gitignore' and changed matching behavior for patterns like foo/*, so pinned-then-bumped projects can silently match different files
- You need general-purpose advanced globbing (brace expansion, extglob, globstar controls) rather than gitignore semantics: wcmatch covers that ground better
- This is a one-person project with a small bus factor; it is steadily maintained but there is no team behind it, which matters if you build a core product feature on its exact matching behavior
Setup reality
pip install pathspec is as easy as installs get: pure Python, no compiled parts, Python 3.9+. The gotchas are semantic. You must pick between PathSpec.from_lines('gitignore', ...) and GitIgnoreSpec.from_lines(...), and the two disagree on real-world patterns; if the goal is 'behave like Git', use GitIgnoreSpec, full stop. Paths are matched as relative strings, so passing absolute paths against patterns written for a repo root quietly matches nothing. Directory patterns need the trailing slash exactly as in gitignore. And if you came from 0.x, 'gitwildmatch' still works but is deprecated with different edge-case behavior than the new 'gitignore' type, which is an easy source of confusing diffs.
Patterns
Compile gitignore-style patternscompile-gitignore-lines
from pathspec import PathSpec
spec = PathSpec.from_lines('gitignore', [
'*.pyc',
'build/',
'!build/keep.txt',
])
print(spec.match_file('module.pyc')) # TrueSince 1.0 the pattern type is named 'gitignore'; the old 'gitwildmatch' name still works but is deprecated and keeps slightly different edge-case behavior.
Load patterns from a .gitignore fileload-gitignore-file
from pathspec import GitIgnoreSpec
with open('.gitignore') as fh:
spec = GitIgnoreSpec.from_lines(fh)
print(spec.match_file('dist/app.tar.gz'))from_lines accepts any iterable of lines, so the open file object can be passed directly; comments and blank lines are handled like Git does.
Match exactly like Git, edge cases includedexact-git-behavior
from pathspec import GitIgnoreSpec
spec = GitIgnoreSpec.from_lines([
'logs/',
'!logs/important.log',
])
print(spec.match_file('logs/debug.log')) # True
print(spec.match_file('logs/important.log')) # False (re-included)GitIgnoreSpec replicates Git's real behavior, including negations inside excluded directories, where plain PathSpec follows the documentation instead.
Check one pathmatch-single-file
from pathlib import PurePath
from pathspec import PathSpec
spec = PathSpec.from_lines('gitignore', ['docs/**/*.md'])
spec.match_file('docs/guide/intro.md') # True
spec.match_file(PurePath('docs/README.md')) # pathlib works tooPaths are matched as given, relative to the implied root of the patterns; absolute paths against repo-relative patterns will not match.
Filter an existing list of pathsfilter-file-list
from pathspec import PathSpec
spec = PathSpec.from_lines('gitignore', ['*.tmp', 'cache/'])
files = ['a.txt', 'b.tmp', 'cache/x.json']
ignored = set(spec.match_files(files))
kept = [f for f in files if f not in ignored]match_files returns a generator of the matching (ignored) entries; wrap it in set or list before reusing it.
Walk a directory tree for matcheswalk-directory-tree
from pathspec import PathSpec
spec = PathSpec.from_lines('gitignore', ['**/*.log'])
for rel_path in spec.match_tree_files('/var/app'):
print(rel_path)Yields paths relative to the root you pass in, walking the filesystem for you; use match_files instead if you already have the path list.
Invert matching to select files to keepkeep-instead-of-ignore
from pathspec import GitIgnoreSpec
spec = GitIgnoreSpec.from_lines(open('.gitignore'))
keep = set(spec.match_tree_files('project/', negate=True))Specs positively match the ignored files by default; negate=True flips the result, which is what backup and packaging tools usually want.
Combine multiple pattern sourcescombine-specs
from pathspec import PathSpec
base = PathSpec.from_lines('gitignore', open('.gitignore'))
local = PathSpec.from_lines('gitignore', open('.gitignore.local'))
spec = base + local # later patterns win, like nested .gitignore filesThe + operator concatenates pattern lists, preserving order, so later negations can re-include files excluded by earlier specs.
Speed up matching with an alternate regex backendfaster-backend
# pip install 'pathspec[re2]'
from pathspec import GitIgnoreSpec
spec = GitIgnoreSpec.from_lines(lines, backend='re2')
# or backend='hyperscan', or the default backend='best'New in 1.0. The default 'best' picks the fastest installed backend; re2 tends to win at high pattern counts per the project's own benchmarks.
Find out which pattern matchedcheck-which-pattern-matched
from pathspec import PathSpec
spec = PathSpec.from_lines('gitignore', ['*.log', '!keep.log'])
result = spec.check_file('debug.log')
print(result.include) # True if ignored
print(result.index) # index of the deciding patterncheck_file returns a result object instead of a bare bool, which is what you need for 'why is this file ignored' debugging output.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| gitignore-parser | PyPI | You want a tiny single-purpose parser for one .gitignore file and do not need tree walking or combined specs. |
| wcmatch | PyPI | You need advanced general globbing (extglob, brace expansion, globstar) beyond gitignore semantics. |