Eliminar un árbol de directorios parece sencillo hasta que aparecen archivos protegidos, permisos cambiantes, enlaces simbólicos, procesos concurrentes o fallos parciales. shutil.rmtree() realiza la eliminación recursiva y el parámetro onexc ofrece un punto central para tratar excepciones de forma previsible. Es útil en scripts de limpieza, pruebas automatizadas, instaladores, pipelines de datos, despliegues y aplicaciones que crean espacios de trabajo temporales.
Esta guía explica cómo usar shutil.rmtree con onexc, cómo interpretar los argumentos del callback, cuándo conviene reintentar, cuándo registrar un error y cuándo detener la operación. El objetivo no es solamente borrar carpetas, sino hacerlo con seguridad, observabilidad y un comportamiento reproducible.
Qué hace shutil.rmtree
shutil.rmtree(ruta) elimina un directorio y todo su contenido. A diferencia de Path.rmdir() u os.rmdir(), no exige que la carpeta esté vacía. Esa capacidad es cómoda, pero también peligrosa: una ruta incorrecta puede borrar una gran cantidad de datos.
from shutil import rmtree
rmtree("build")
Antes de ejecutarla, valida el destino, evita rutas construidas directamente con entradas externas y no apliques eliminación recursiva a directorios críticos. Para recorridos modernos, consulta pathlib.Path.walk en Python y os.fwalk en Python.
Por qué onexc es importante
Durante la eliminación pueden fallar varias operaciones internas. Un archivo puede ser de solo lectura, un antivirus puede mantenerlo abierto, los permisos pueden cambiar entre la lista y el borrado o otro proceso puede eliminarlo primero. onexc recibe un callback cuando ocurre una excepción.
from shutil import rmtree
def al_fallar(funcion, ruta, error):
print(f"Fallo en {funcion.__name__}: {ruta}: {error}")
rmtree("build", onexc=al_fallar)
El callback recibe la función que falló, la ruta implicada y el objeto de excepción. Esto permite tomar decisiones específicas sin distribuir bloques amplios de try/except por toda la aplicación.
onexc frente a ignore_errors
ignore_errors=True suprime los fallos durante la eliminación. Es sencillo, pero reduce la visibilidad. El programa puede terminar aunque todavía existan archivos y los pasos siguientes pueden asumir un estado limpio que no es real.
rmtree("cache", ignore_errors=True)
onexc es mejor cuando necesitas registrar, reparar, clasificar o relanzar fallos. Ignorar errores puede ser aceptable en una limpieza descartable sin consecuencias, pero no debería ser la opción predeterminada en flujos importantes.
Corregir un archivo de solo lectura
Un caso común es un archivo que no puede eliminarse por sus permisos. El callback puede cambiar el permiso y repetir únicamente la operación que falló.
import os
import stat
from shutil import rmtree
def corregir_permiso(funcion, ruta, error):
if isinstance(error, PermissionError):
os.chmod(ruta, stat.S_IWRITE)
funcion(ruta)
return
raise error
rmtree("salida", onexc=corregir_permiso)
Esta estrategia debe limitarse a un árbol propiedad de la aplicación. Cambiar permisos en destinos arbitrarios puede ampliar el impacto de una configuración equivocada. El callback debe reparar solo situaciones conocidas y relanzar las demás.
No ocultes excepciones desconocidas
Un callback que solo imprime y retorna puede dejar el árbol parcialmente eliminado. En limpiezas críticas, relanza los errores que no puedan corregirse con seguridad.
def tratar(funcion, ruta, error):
if isinstance(error, FileNotFoundError):
return
raise error
FileNotFoundError puede ser inofensivo cuando otro worker ya eliminó el elemento. Un fallo de permisos, un error del sistema de archivos o una ruta inválida pueden indicar un problema mayor. Para contratos explícitos, revisa dataclasses.KW_ONLY en Python.
Registrar contexto útil
En producción conviene reemplazar print() por logging estructurado. Incluye la función, la ruta, el tipo de excepción y el identificador de la ejecución.
import logging
from shutil import rmtree
log = logging.getLogger(__name__)
def registrar(funcion, ruta, error):
log.error(
"fallo al eliminar ruta",
extra={
"operacion": funcion.__name__,
"ruta": ruta,
"tipo_error": type(error).__name__,
},
)
raise error
rmtree("artefactos", onexc=registrar)
Los logs permiten distinguir un bloqueo temporal de un problema permanente de configuración y medir cuántas limpiezas quedan incompletas.
Validar la ruta antes de borrar
Una función segura debe resolver la ruta, compararla con una raíz permitida y rechazar directorios críticos.
from pathlib import Path
from shutil import rmtree
RAIZ = Path("/srv/mi-app/work").resolve()
def borrar_subdirectorio(valor):
destino = (RAIZ / valor).resolve()
if destino == RAIZ or RAIZ not in destino.parents:
raise ValueError("Ruta fuera de la raíz permitida")
rmtree(destino)
La comprobación bloquea recorridos como ../ y evita que un valor vacío seleccione la propia raíz. Para espacios temporales, consulta tempfile en Python.
Enlaces simbólicos y ataques de ruta
Las operaciones recursivas requieren atención a enlaces simbólicos y condiciones de carrera. Las implementaciones modernas de Python utilizan protecciones adicionales en plataformas compatibles, pero la aplicación aún debe controlar el origen de los destinos y limitar la limpieza a directorios propios.
No ejecutes una limpieza privilegiada sobre rutas proporcionadas por usuarios. Cuando el proceso tiene permisos elevados, un error de validación puede alcanzar archivos que normalmente estarían protegidos.
Reintentos cortos y limitados
Algunos fallos son temporales porque otro proceso mantiene el archivo abierto. Una política de reintentos debe ser pequeña, limitada y observable.
import time
from shutil import rmtree
class Tratador:
def __init__(self, intentos=2):
self.restantes = intentos
def __call__(self, funcion, ruta, error):
if self.restantes and isinstance(error, PermissionError):
self.restantes -= 1
time.sleep(0.2)
funcion(ruta)
return
raise error
rmtree("cache", onexc=Tratador())
No conviertas el callback en un bucle infinito. Si la condición persiste, falla claramente y conserva contexto suficiente para el diagnóstico.
Uso en pruebas automatizadas
Las pruebas crean directorios temporales y deben eliminarlos incluso cuando una aserción falla. Es preferible usar context managers y fixtures, dejando rmtree como mecanismo controlado.
from pathlib import Path
from tempfile import mkdtemp
from shutil import rmtree
carpeta = Path(mkdtemp())
try:
(carpeta / "datos.txt").write_text("prueba", encoding="utf-8")
finally:
rmtree(carpeta)
Cuando varios recursos deben cerrarse dinámicamente, contextlib.ExitStack en Python permite registrar callbacks de limpieza con flexibilidad.
Limpieza idempotente
Una rutina idempotente puede ejecutarse más de una vez sin producir un estado inválido. Una estrategia básica consiste en aceptar que el directorio ya no exista.
from pathlib import Path
from shutil import rmtree
def eliminar_si_existe(ruta):
destino = Path(ruta)
if destino.exists():
rmtree(destino)
La verificación previa no elimina las carreras porque el estado puede cambiar inmediatamente después. El callback todavía puede tolerar FileNotFoundError si ese resultado es válido.
Callbacks fáciles de probar
Mantén el callback pequeño y determinista. Separa la política de los efectos secundarios. Una función puede decidir si una excepción es recuperable, otra puede reparar y otra registrar el fallo final. Así las pruebas unitarias son más simples y el callback no se transforma en un manejador desordenado.
def recuperable(error):
return isinstance(error, (FileNotFoundError, PermissionError))
Las pruebas deben cubrir un archivo ausente, un archivo protegido, una excepción inesperada y un reintento que también falla. Confirma que los errores desconocidos no sean silenciados.
Compatibilidad de versiones
onexc está disponible en versiones modernas de Python como callback más claro para las excepciones de rmtree. Comprueba la versión mínima del proyecto. Una biblioteca compatible con entornos antiguos puede necesitar una capa de compatibilidad.
La documentación oficial de shutil.rmtree explica la firma actual, los detalles de seguridad y el comportamiento de las excepciones. La documentación de pathlib complementa la validación de rutas.
Una función de limpieza más segura
import logging
import os
import stat
from pathlib import Path
from shutil import rmtree
log = logging.getLogger(__name__)
def limpiar(ruta, raiz):
raiz = Path(raiz).resolve()
destino = Path(ruta).resolve()
if destino == raiz or raiz not in destino.parents:
raise ValueError("Destino no permitido")
def onexc(funcion, valor, error):
if isinstance(error, FileNotFoundError):
return
if isinstance(error, PermissionError):
os.chmod(valor, stat.S_IWRITE)
funcion(valor)
return
log.exception("limpieza incompleta", extra={"ruta": str(valor)})
raise error
rmtree(destino, onexc=onexc)
La función valida la raíz permitida, tolera la desaparición concurrente, repara un caso conocido de permisos y relanza cualquier situación inesperada.
Salvaguardas operativas
Considera un modo de simulación que enumere los destinos sin eliminarlos. En tareas programadas, registra el número de archivos antes y después. Para datos valiosos, mueve primero el directorio a una zona de cuarentena y elimínalo después de una validación. Estas prácticas reducen el impacto de un error y ofrecen una oportunidad de recuperación.
No uses la eliminación recursiva como sustituto de una política de retención. Define qué directorios pueden borrarse, qué antigüedad deben tener y qué proceso es su propietario.
Buenas prácticas
Resuelve la ruta antes de borrar. Limita la operación a una raíz controlada. No pases entradas externas directamente. Relanza errores desconocidos. Registra contexto suficiente. Evita reintentos infinitos. Prueba archivos protegidos y directorios ya eliminados. Confirma la versión de Python. No combines ignore_errors=True con una expectativa de eliminación completa.
Conclusión
shutil.rmtree es una herramienta directa para eliminar árboles de directorios y onexc convierte fallos imprevisibles en decisiones explícitas. Con validación de rutas, logs, reparaciones limitadas y excepciones relanzadas, la limpieza es mucho más confiable.
Comienza con un callback que registre y relance. Añade solamente recuperaciones que comprendas y puedas probar. Así la automatización no oculta árboles parcialmente eliminados ni amplía el impacto de una ruta incorrecta.







