Summary
In many Python projects we need to clean a directory while excluding files that match a pattern (e.g., keep *.pyc or *.idea folders). While shutil.copytree() offers an ignore= parameter, shutil.rmtree() does not. The solution is to walk the directory tree manually and selectively delete files and directories that do not match the ignore patterns.
Root Cause
shutil.rmtree() performs an immediate, unconditional depth‑first deletion. It has no hook for filtering which paths to delete, so every file and subdirectory under the target is removed. This lack of an ignore mechanism forces developers to write custom deletion logic.
Why This Happens in Real Systems
- Zero‑config deletion:
rmtreeis designed for quick, clean removal of temporary build directories or deployment artifacts. - Performance: Custom filtering introduces overhead; the built‑in implementation is highly optimized for speed.
- API Design Decision: The Python standard library prioritizes minimal complexity for a highly‑used utility, leaving advanced filtering to the developer.
Real-World Impact
- Accidental loss: Developers might delete critical cache files or user data when running cleanup scripts.
- Inconsistent builds: Tests that rely on preserved files (
*.pyc,.cache) can fail if those files are unintentionally removed. - Maintenance burden: Without a built‑in ignore, teams must maintain custom pruning code, increasing bug introduction risk.
Example or Code (if necessary and relevant)
import os
import shutil
from fnmatch import fnmatch
def rmtree_ignore(path, ignored_patterns):
"""
Remove directory tree at `path`, ignoring any entry whose name
matches one of the glob patterns in `ignored_patterns`.
"""
for root, dirs, files in os.walk(path, topdown=False):
# Delete files first
for name in files:
full = os.path.join(root, name)
if any(fnmatch(name, pat) for pat in ignored_patterns):
continue
os.remove(full)
# Delete directories next
for name in dirs:
full = os.path.join(root, name)
if any(fnmatch(name, pat) for pat in ignored_patterns):
continue
os.rmdir(full)
# Finally, delete the root itself if it wasn't ignored
if not any(fnmatch(os.path.basename(path), pat) for pat in ignored_patterns):
os.rmdir(path)
# Usage example
rmtree_ignore("/tmp/foo", ignored_patterns=["*.pyc", "tmp*", ".git"])
How Senior Engineers Fix It
- Wrap
os.walkin a reusable helper that accepts glob patterns and usesfnmatchfor matching. - Leverage
pathlibfor clearer path handling and more expressive code. - Add logging to audit which files were skipped or deleted, aiding debugging.
- Write unit tests covering various directory structures and pattern combinations to ensure no accidental deletions.
- Document the behavior in the project’s maintenance guides, so new contributors understand the custom delete logic.
Why Juniors Miss It
- Assumption of
shutil.rmtree: Junior developers expect a single API call to behave likecopytree(ignore=…). - Missing awareness of glob patterns: They may not realize
fnmatchor regex can enforce ignore rules. - Focus on speed over safety: A quick script that deletes everything seems simpler than adding a few lines of filtered logic.
- Debugging difficulty: Without logs or tests, they might not notice that critical files are being removed until a downstream failure occurs.