El módulo shutil forma parte de la biblioteca estándar de Python y reúne funciones de alto nivel para copiar, mover, eliminar y empaquetar archivos y carpetas. Es especialmente útil en automatizaciones, instaladores, sistemas de respaldo, organizadores de documentos y tareas de mantenimiento. Aunque sus operaciones son sencillas, trabajar con datos reales exige protegerse contra sobrescrituras, permisos insuficientes, rutas equivocadas y copias incompletas.
Para aprovecharlo bien, conviene entender el manejo de rutas con pathlib, el módulo os, el tratamiento de excepciones en Python y el uso de logging. La documentación oficial de shutil contiene la referencia completa, y la documentación de pathlib explica la forma moderna de representar rutas.
Importar los módulos
import shutil
from pathlib import Pathpathlib mejora la legibilidad y permite construir rutas de forma portable. Aunque shutil acepta objetos de ruta, en algunas integraciones antiguas puede ser necesario convertirlos con str().
Copiar un archivo con copy
origen = Path("entrada/informe.pdf")
destino = Path("salida/informe.pdf")
destino.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(origen, destino)copy() copia el contenido y los permisos básicos. Si el destino es una carpeta, conserva el nombre original. Si es una ruta de archivo, usa el nombre especificado.
Conservar más metadatos con copy2
shutil.copy2(origen, destino)copy2() intenta conservar metadatos adicionales, como las fechas de acceso y modificación. Esto resulta útil en respaldos y migraciones. Sin embargo, no todos los sistemas de archivos admiten los mismos atributos, por lo que no debes asumir una réplica perfecta de permisos, propietarios o listas de control.
Copiar únicamente el contenido
with origen.open("rb") as entrada, destino.open("wb") as salida:
shutil.copyfileobj(entrada, salida)copyfile() y copyfileobj() se centran en los datos. copyfileobj() también permite trabajar con objetos de archivo y controlar el tamaño del búfer. Para la mayoría de los casos, copy2() es una elección práctica.
Evitar sobrescrituras accidentales
def destino_disponible(ruta: Path) -> Path:
if not ruta.exists():
return ruta
contador = 1
while True:
candidato = ruta.with_name(
f"{ruta.stem}_{contador}{ruta.suffix}"
)
if not candidato.exists():
return candidato
contador += 1Antes de copiar:
destino = destino_disponible(Path("salida/informe.pdf"))
shutil.copy2(origen, destino)En aplicaciones empresariales también puedes calcular un hash y evitar duplicar archivos idénticos.
Mover archivos y carpetas
shutil.move("entrada/foto.jpg", "archivo/imagenes/foto.jpg")Si origen y destino están en el mismo sistema de archivos, la operación suele ser un renombrado rápido. Si están en discos diferentes, Python puede copiar y después eliminar el original. Esto significa que mover un archivo grande puede tardar y fallar a mitad del proceso.
Validar antes de mover
def mover_seguro(origen: Path, destino: Path) -> Path:
if not origen.exists():
raise FileNotFoundError(f"No existe: {origen}")
if not origen.is_file():
raise ValueError("El origen debe ser un archivo")
destino.parent.mkdir(parents=True, exist_ok=True)
destino = destino_disponible(destino)
resultado = shutil.move(str(origen), str(destino))
return Path(resultado)La función evita reemplazar un archivo existente y crea la carpeta necesaria.
Copiar una carpeta completa
shutil.copytree(
"proyecto",
"respaldo/proyecto",
dirs_exist_ok=False
)Por defecto, el destino no debe existir. Desde versiones modernas de Python, dirs_exist_ok=True permite copiar dentro de una carpeta existente. Úsalo con cuidado, porque archivos con el mismo nombre pueden reemplazarse.
Excluir archivos durante copytree
shutil.copytree(
"proyecto",
"respaldo/proyecto",
ignore=shutil.ignore_patterns(
"__pycache__",
"*.pyc",
".venv",
".git"
)
)Este patrón es útil para respaldar código sin copiar entornos virtuales, cachés o repositorios internos.
Eliminar carpetas con rmtree
shutil.rmtree("carpeta_temporal")rmtree() elimina una carpeta y todo su contenido. Es una de las funciones más peligrosas del módulo. Antes de utilizarla, valida que la ruta se encuentre dentro de una ubicación permitida:
BASE_TEMP = Path("temporales").resolve()
objetivo = Path("temporales/tarea_123").resolve()
if BASE_TEMP not in objetivo.parents:
raise ValueError("Ruta fuera del directorio temporal")
shutil.rmtree(objetivo)No construyas rutas de borrado directamente con entradas no confiables.
Comprobar espacio disponible
total, usado, libre = shutil.disk_usage(Path.home())
print(f"Libre: {libre / 1024**3:.2f} GB")Antes de copiar grandes volúmenes, compara el tamaño estimado con el espacio libre. Mantén un margen adicional para archivos temporales.
Crear archivos ZIP
shutil.make_archive(
base_name="respaldo_2026_07_11",
format="zip",
root_dir="datos"
)Para extraer:
shutil.unpack_archive("respaldo.zip", "restaurado")No extraigas archivos recibidos de fuentes desconocidas sin validar sus rutas internas, porque un archivo malicioso puede intentar escribir fuera de la carpeta de destino.
Comparar tamaños después de copiar
shutil.copy2(origen, destino)
if origen.stat().st_size != destino.stat().st_size:
raise IOError("La copia no conserva el tamaño esperado")Comparar tamaños detecta fallos evidentes, pero no garantiza que el contenido sea idéntico. Para una verificación más fuerte, calcula un hash:
import hashlib
def sha256(ruta: Path) -> str:
digest = hashlib.sha256()
with ruta.open("rb") as archivo:
for bloque in iter(lambda: archivo.read(1024 * 1024), b""):
digest.update(bloque)
return digest.hexdigest()Registrar cada operación
import logging
logging.basicConfig(
filename="archivos.log",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
encoding="utf-8"
)
try:
nuevo = mover_seguro(origen, destino)
logging.info("Movido %s a %s", origen, nuevo)
except OSError:
logging.exception("Error moviendo %s", origen)El registro facilita restaurar movimientos y entender errores de permisos o disco lleno.
Ejemplo: clasificar archivos por extensión
CATEGORIAS = {
".pdf": "documentos",
".docx": "documentos",
".jpg": "imagenes",
".png": "imagenes",
".zip": "comprimidos",
}
def organizar(carpeta: Path):
for archivo in carpeta.iterdir():
if not archivo.is_file():
continue
categoria = CATEGORIAS.get(
archivo.suffix.lower(),
"otros"
)
destino = carpeta / categoria / archivo.name
mover_seguro(archivo, destino)Prueba primero con copias de archivos. Añade exclusiones para descargas incompletas, logs y el propio script.
Modo simulación
def mover_seguro(origen, destino, simulacion=True):
destino = destino_disponible(destino)
if simulacion:
print(f"[SIMULACIÓN] {origen} -> {destino}")
return destino
destino.parent.mkdir(parents=True, exist_ok=True)
return Path(shutil.move(str(origen), str(destino)))El modo simulación es una de las mejores defensas antes de ejecutar una automatización sobre miles de archivos.
Errores frecuentes
Los problemas más comunes son sobrescribir sin aviso, eliminar una ruta equivocada con rmtree(), ignorar el espacio disponible, copiar carpetas recursivamente dentro de sí mismas, mover archivos todavía abiertos y asumir que una operación entre discos es instantánea. También debes manejar PermissionError, FileNotFoundError y errores generales de entrada y salida.
Conclusión
shutil simplifica tareas que serían más complejas con operaciones de bajo nivel. copy2() copia archivos conservando metadatos, move() traslada elementos, copytree() replica carpetas y make_archive() crea paquetes comprimidos. Para usar estas funciones de forma confiable, valida rutas, evita sobrescrituras, registra acciones, comprueba resultados y ofrece un modo simulación. Así podrás automatizar copias, migraciones y organización de documentos sin poner los datos en riesgo.






