How to Delete an Entire Directory Tree in Python
Deleting a folder that contains files and subdirectories requires recursive removal. Python's built-in os.rmdir() and Path.rmdir() only work on empty directories and raise an error if any contents remain. To remove an entire directory tree, including everything inside it, you need shutil.rmtree().
In this guide, you will learn how to delete directory trees safely, handle read-only files, protect against accidental deletion of important paths, and choose the right method for your specific situation.
Using shutil.rmtree()
The shutil.rmtree() function is the standard approach for recursively deleting a directory and all of its contents:
import shutil
from pathlib import Path
target = Path("temp_build")
if target.exists():
shutil.rmtree(target)
print(f"Deleted: {target}")
else:
print(f"Directory does not exist: {target}")
The same operation using string paths and os.path:
import shutil
import os
folder_path = "cache/old_data"
if os.path.exists(folder_path):
shutil.rmtree(folder_path)
print("Deleted successfully")
shutil.rmtree() permanently deletes all files and subdirectories without moving them to the Recycle Bin or Trash. There is no undo. Always verify the path before calling this function.
Why os.rmdir() Is Not Enough
A common mistake is trying to use os.rmdir() or Path.rmdir() on a directory that still has contents:
import os
# This raises an error if the directory is not empty
try:
os.rmdir("project_folder")
except OSError as e:
print(f"Error: {e}")
Output:
Error: [Errno 39] Directory not empty: 'project_folder'
These functions only remove empty directories. For anything containing files or subdirectories, you need shutil.rmtree().
Ignoring Errors During Cleanup
For cleanup scripts where failures should not crash the application, use the ignore_errors parameter:
import shutil
# Silently ignore all errors (missing dirs, locked files, etc.)
shutil.rmtree("logs", ignore_errors=True)
shutil.rmtree("cache", ignore_errors=True)
shutil.rmtree("temp", ignore_errors=True)
print("Cleanup complete")
Output:
Cleanup complete
This handles cases where directories do not exist, files are locked by another process, or permissions prevent deletion. The function simply skips anything it cannot remove.
Handling Read-Only Files
On Windows, shutil.rmtree() fails when it encounters read-only files, which is common in Git repositories where the .git/objects directory contains read-only files. An error handler can fix permissions and retry:
import shutil
import os
import stat
def handle_remove_readonly(func, path, exc_info):
"""Error handler that removes read-only flag and retries."""
if not os.access(path, os.W_OK):
os.chmod(path, stat.S_IWRITE)
func(path)
else:
raise exc_info[1]
# Delete a Git repository including read-only object files
shutil.rmtree(".git", onerror=handle_remove_readonly)
print("Repository deleted")
Starting with Python 3.12, the onerror parameter is deprecated in favor of onexc, which receives the exception directly instead of exc_info:
import shutil
import os
import stat
def handle_error(func, path, exc):
"""Error handler for Python 3.12+."""
if isinstance(exc, PermissionError):
os.chmod(path, stat.S_IWRITE)
func(path)
else:
raise exc
shutil.rmtree(".git", onexc=handle_error)
Safe Deletion with Confirmation
For interactive scripts, add safety checks and user confirmation before deleting:
import shutil
from pathlib import Path
def safe_delete_directory(path: str, require_confirmation: bool = True) -> bool:
"""Safely delete a directory with optional confirmation."""
target = Path(path)
if not target.exists():
print(f"Directory does not exist: {path}")
return False
if not target.is_dir():
print(f"Path is not a directory: {path}")
return False
# Count contents
file_count = sum(1 for item in target.rglob("*") if item.is_file())
dir_count = sum(1 for item in target.rglob("*") if item.is_dir())
if require_confirmation:
print(f"About to delete: {target.absolute()}")
print(f"Contains: {file_count} files, {dir_count} subdirectories")
response = input("Proceed? (yes/no): ")
if response.lower() != "yes":
print("Cancelled.")
return False
shutil.rmtree(target)
print(f"Deleted: {target}")
return True
safe_delete_directory("old_project")
Example interaction:
About to delete: /home/user/old_project
Contains: 47 files, 12 subdirectories
Proceed? (yes/no): yes
Deleted: old_project
Protecting Against Dangerous Paths
A critical safety measure is preventing accidental deletion of important system directories. A validation layer catches dangerous paths before any deletion occurs:
import shutil
from pathlib import Path
PROTECTED_PATHS = {
Path.home(),
Path("/"),
Path("C:\\"),
Path.home() / "Documents",
Path.home() / "Desktop",
}
def delete_directory_safely(path: str) -> None:
"""Delete a directory with protection against dangerous paths."""
target = Path(path).resolve()
for protected in PROTECTED_PATHS:
if target == protected.resolve():
raise ValueError(f"Cannot delete protected path: {target}")
if not target.exists():
print(f"Nothing to delete: {target}")
return
if not target.is_dir():
raise ValueError(f"Path is not a directory: {target}")
shutil.rmtree(target)
print(f"Deleted: {target}")
Always use .resolve() to convert paths to their absolute form before comparing. This prevents path traversal tricks like ../../important_folder from bypassing your safety checks.
Deleting Only the Contents of a Directory
Sometimes you want to empty a directory without removing the directory itself. This is common for cache or temp folders that should continue to exist:
import shutil
from pathlib import Path
def clear_directory(path: str) -> int:
"""Remove all contents but keep the directory itself."""
target = Path(path)
deleted = 0
if not target.exists():
return 0
for item in target.iterdir():
if item.is_dir():
shutil.rmtree(item)
else:
item.unlink()
deleted += 1
return deleted
count = clear_directory("temp")
print(f"Removed {count} items from temp/")
Example output:
Removed 5 items from temp/
Moving to Trash Instead of Permanent Deletion
For recoverable deletion that moves files to the system Recycle Bin or Trash, use the send2trash library:
pip install send2trash
from send2trash import send2trash
from pathlib import Path
target = Path("project_backup")
if target.exists():
send2trash(str(target))
print(f"Moved to trash: {target}")
This is a safer option during development or when you are not entirely sure the deletion is correct. Files can be recovered from the Trash if needed.
Method Comparison
| Method | Recursive | Handles Non-Empty Dirs | Recoverable |
|---|---|---|---|
shutil.rmtree() | Yes | Yes | No |
os.rmdir() | No | No (empty only) | No |
Path.rmdir() | No | No (empty only) | No |
Path.unlink() | No | Files only | No |
send2trash() | Yes | Yes | Yes |
Quick reference for each use case:
import os
import shutil
from pathlib import Path
# Empty directory only
os.rmdir("empty_folder")
Path("empty_folder").rmdir()
# Single file only
os.remove("file.txt")
Path("file.txt").unlink()
# Full directory tree (permanent)
shutil.rmtree("folder_with_contents")
# Full directory tree (recoverable)
from send2trash import send2trash
send2trash("folder_with_contents")
Conclusion
- Use
shutil.rmtree()for standard recursive directory deletion. - Add the
ignore_errors=Trueparameter for cleanup scripts where failures should not interrupt execution. - Use a custom error handler with
onerror(oronexcin Python 3.12+) to handle read-only files, especially in Git repositories on Windows. - Always validate paths against a set of protected directories before deletion to prevent catastrophic mistakes.
- For situations where you might need to recover the deleted files, use
send2trashto move them to the system Trash instead of permanently removing them.