Skip to main content

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")
warning

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")
Python 3.12+ Change

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}")
tip

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

MethodRecursiveHandles Non-Empty DirsRecoverable
shutil.rmtree()YesYesNo
os.rmdir()NoNo (empty only)No
Path.rmdir()NoNo (empty only)No
Path.unlink()NoFiles onlyNo
send2trash()YesYesYes

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=True parameter for cleanup scripts where failures should not interrupt execution.
  • Use a custom error handler with onerror (or onexc in 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 send2trash to move them to the system Trash instead of permanently removing them.