Skip to content

How to Delete an Entire Directory Tree in Python

Learn how to safely remove non-empty directories in Python using shutil.rmtree(), handle read-only files, and implement safeguards against accidental deletions.

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

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

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

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]

shutil.rmtree(".git", onerror=handle_remove_readonly)
print("Repository deleted")

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

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

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

os.rmdir("empty_folder")
Path("empty_folder").rmdir()

os.remove("file.txt")
Path("file.txt").unlink()

shutil.rmtree("folder_with_contents")

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.
Navigation