shutil.rmtree onexc: Handle Directory Removal Errors

Published on: September 10, 2026
Reading time: 6 minutes
Python code for safe directory cleanup with shutil.rmtree

Deleting a directory tree looks simple until protected files, changing permissions, symbolic links, concurrent processes, or partial failures appear. shutil.rmtree() handles recursive deletion, while the onexc parameter gives your application a central place to process exceptions predictably. This is useful in cleanup scripts, automated tests, installers, data pipelines, deployment jobs, and applications that create temporary workspaces.

This guide explains how to use shutil.rmtree with onexc, how to understand the callback arguments, when a retry is reasonable, when an error should be logged, and when the operation must stop. The goal is not merely to remove folders, but to do it with safety, observability, and reproducible behavior.

What shutil.rmtree does

shutil.rmtree(path) deletes a directory and everything below it. Unlike Path.rmdir() or os.rmdir(), the directory does not need to be empty. That makes the function convenient and powerful, but also dangerous: an incorrect path can remove a large amount of data.

from shutil import rmtree

rmtree("build")

Before calling it, validate the target, avoid paths built directly from untrusted input, and never apply recursive deletion to a critical directory. For modern path traversal patterns, see Python pathlib.Path.walk and Python os.fwalk.

Why onexc matters

Several internal operations can fail during recursive deletion. A file may be read-only, an antivirus program may keep it open, permissions may change between listing and deletion, or another process may remove the item first. The onexc parameter receives a callback whenever an exception occurs.

from shutil import rmtree

def on_failure(function, path, error):
    print(f"Failed in {function.__name__}: {path}: {error}")

rmtree("build", onexc=on_failure)

The callback receives the function that failed, the path involved, and the exception object. This lets you make focused decisions without scattering broad try/except blocks across the application.

onexc versus ignore_errors

ignore_errors=True suppresses failures during removal. It is simple, but it also removes visibility. The program may finish while files remain, and later steps can assume a clean state that does not actually exist.

rmtree("cache", ignore_errors=True)

onexc is better when you need to record, repair, classify, or re-raise failures. Ignoring errors can be acceptable for disposable cleanup where an incomplete result has no consequence, but it should not be the default for critical workflows.

Repairing a read-only file

A common case is a file that cannot be deleted because its permissions do not allow modification. A callback can change that permission and retry only the failed operation.

import os
import stat
from shutil import rmtree

def repair_permission(function, path, error):
    if isinstance(error, PermissionError):
        os.chmod(path, stat.S_IWRITE)
        function(path)
        return
    raise error

rmtree("output", onexc=repair_permission)

This approach should be restricted to a directory tree owned by your application. Changing permissions on arbitrary targets can magnify the impact of a configuration error. The callback should repair only known situations and re-raise everything else.

Do not hide unknown exceptions

A callback that only prints and returns may leave the tree partially removed. In critical cleanup, re-raise errors that cannot be safely corrected.

def handle(function, path, error):
    if isinstance(error, FileNotFoundError):
        return
    raise error

FileNotFoundError may be harmless when another worker already removed the item. A permission failure, filesystem error, invalid path, or unexpected operating-system condition can indicate a larger problem. For explicit API contracts, read Python dataclasses.KW_ONLY.

Logging useful context

Production code should usually replace print() with structured logging. Include the failing function, path, exception type, and execution identifier.

import logging
from shutil import rmtree

log = logging.getLogger(__name__)

def record(function, path, error):
    log.error(
        "failed to remove path",
        extra={
            "operation": function.__name__,
            "path": path,
            "error_type": type(error).__name__,
        },
    )
    raise error

rmtree("artifacts", onexc=record)

Good logs help distinguish a transient lock from a permanent configuration problem. They also let a team measure how often cleanup remains incomplete.

Validate the target before deletion

A safe helper should resolve the path, compare it with an allowed root, and reject critical directories.

from pathlib import Path
from shutil import rmtree

ROOT = Path("/srv/my-app/work").resolve()

def delete_subdirectory(value):
    target = (ROOT / value).resolve()
    if target == ROOT or ROOT not in target.parents:
        raise ValueError("Path is outside the allowed root")
    rmtree(target)

This prevents traversal through values such as ../. It also stops an empty or malformed value from selecting the working root itself. For temporary workspace patterns, see Python tempfile.

Recursive filesystem operations require attention to symbolic links and race conditions. Modern Python implementations use additional protections on supported platforms, but your application must still control where targets come from and limit cleanup to directories it owns.

Do not run privileged cleanup against user-supplied paths. When a process has elevated permissions, a single validation mistake can reach files that would normally be protected.

Short and bounded retries

Some failures are temporary because another process still holds a file open. A retry policy should be small, bounded, and visible.

import time
from shutil import rmtree

class Handler:
    def __init__(self, retries=2):
        self.remaining = retries

    def __call__(self, function, path, error):
        if self.remaining and isinstance(error, PermissionError):
            self.remaining -= 1
            time.sleep(0.2)
            function(path)
            return
        raise error

rmtree("cache", onexc=Handler())

Never turn the callback into an infinite loop. If the condition persists, fail clearly and preserve enough context for diagnosis.

Using rmtree in automated tests

Tests frequently create temporary directories and need to remove them even when an assertion fails. Prefer context managers and fixtures, with rmtree as a controlled cleanup mechanism.

from pathlib import Path
from tempfile import mkdtemp
from shutil import rmtree

folder = Path(mkdtemp())
try:
    (folder / "data.txt").write_text("test", encoding="utf-8")
finally:
    rmtree(folder)

When several resources must be closed dynamically, Python contextlib.ExitStack provides a flexible way to register cleanup callbacks.

Idempotent cleanup

An idempotent cleanup routine can run more than once without producing an invalid state. One basic strategy is to handle the case where the directory is already gone.

from pathlib import Path
from shutil import rmtree

def remove_if_present(path):
    target = Path(path)
    if target.exists():
        rmtree(target)

The pre-check does not eliminate race conditions because the state may change immediately afterward. The callback can still tolerate FileNotFoundError when that outcome is acceptable.

Designing callbacks that are testable

Keep the callback small and deterministic. Separate policy from side effects. One function can decide whether an exception is recoverable, another can perform the repair, and a third can log the final failure. This makes unit testing easier and prevents a large callback from becoming an unstructured error handler.

def recoverable(error):
    return isinstance(error, (FileNotFoundError, PermissionError))

Tests should cover a missing file, a read-only file, an unexpected exception, and a retry that still fails. Verify that unexpected errors are not swallowed.

Version compatibility

onexc is available in modern Python versions as a clearer exception callback for rmtree. Check the minimum Python version of your project before adopting it. A library that supports older environments may need a compatibility layer or a different callback strategy.

The official shutil.rmtree documentation describes the current signature, security details, and exception behavior. The pathlib documentation is a complementary reference for resolving and validating paths.

A safer cleanup helper

import logging
import os
import stat
from pathlib import Path
from shutil import rmtree

log = logging.getLogger(__name__)

def cleanup(path, root):
    root = Path(root).resolve()
    target = Path(path).resolve()
    if target == root or root not in target.parents:
        raise ValueError("Target is not allowed")

    def onexc(function, value, error):
        if isinstance(error, FileNotFoundError):
            return
        if isinstance(error, PermissionError):
            os.chmod(value, stat.S_IWRITE)
            function(value)
            return
        log.exception("incomplete cleanup", extra={"path": str(value)})
        raise error

    rmtree(target, onexc=onexc)

The helper validates the allowed root, tolerates concurrent disappearance, repairs one known permission case, and re-raises everything unexpected.

Operational safeguards

Consider adding a dry-run mode that lists intended targets without deleting them. In scheduled jobs, record the number of files before and after cleanup. For high-value data, move a directory to a quarantine area first and delete it only after validation. These patterns reduce the blast radius of a mistake and create a window for recovery.

Do not use recursive deletion as a substitute for retention policy. Define which directories may be deleted, how old they must be, and which process owns them. A robust cleanup job has both technical checks and business rules.

Best practices

Resolve the path before deletion. Restrict operations to a controlled root. Never pass external input directly. Re-raise unknown errors. Log enough context. Avoid infinite retries. Test protected files and already-removed directories. Confirm the Python version. Do not combine ignore_errors=True with an expectation of complete deletion.

Conclusion

shutil.rmtree is a direct tool for deleting directory trees, and onexc turns unpredictable failures into explicit policy decisions. With path validation, logs, narrow repairs, and re-raised exceptions, cleanup becomes far more reliable.

Start with a callback that records and re-raises. Add only recovery behaviors that you understand and can test. This prevents automation from hiding partially removed trees or expanding the impact of an incorrect path.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Server components representing isolated Python interpreters running in parallel
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    InterpreterPoolExecutor: True Parallelism in Python

    Learn Python InterpreterPoolExecutor for CPU-bound tasks, isolated interpreters, true parallelism, and safer concurrency design.

    Ler mais

    Tempo de leitura: 6 minutos
    13/09/2026
    Microprocessor representing CPUs available to a Python process
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    os.process_cpu_count: Count Available CPUs

    Learn Python os.process_cpu_count to size workers according to the CPUs actually available to the process.

    Ler mais

    Tempo de leitura: 4 minutos
    12/09/2026
    Laptop with digital code representing SQLite BLOB data
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    sqlite3.Blob: Incremental BLOB Reads and Writes

    Learn Python sqlite3.Blob for incremental BLOB reads and writes, lower memory use, and safer binary data handling in SQLite.

    Ler mais

    Tempo de leitura: 5 minutos
    12/09/2026
    Statistical analysis for Python random.binomialvariate
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    random.binomialvariate: Simulate Binomial Outcomes

    Learn Python random.binomialvariate to simulate successes, validate probabilities, and analyze binomial scenarios with practical examples.

    Ler mais

    Tempo de leitura: 5 minutos
    11/09/2026
    Python source code and function signature analysis
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    inspect.signature.bind: Validate Function Arguments

    Learn Python inspect.signature.bind to validate arguments, apply defaults, and build safer decorators and dynamic APIs.

    Ler mais

    Tempo de leitura: 4 minutos
    11/09/2026
    Data analytics chart for Python statistics.kde
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    statistics.kde: Estimate Probability Densities

    Learn Python statistics.kde to estimate densities, choose bandwidths, compare kernels, and interpret distributions responsibly.

    Ler mais

    Tempo de leitura: 6 minutos
    10/09/2026