Automatizar copias de seguridad con Python es una forma práctica de proteger documentos, proyectos y configuraciones frente a eliminaciones accidentales, fallos del disco o cambios incorrectos. Un buen sistema de backup no consiste únicamente en copiar una carpeta: debe conservar una estructura clara, evitar sobrescrituras peligrosas, registrar lo ocurrido y permitir una restauración comprobable.
En esta guía construiremos una solución incremental con módulos de la biblioteca estándar. Utilizaremos pathlib para manejar rutas, shutil para copiar, datetime para crear versiones y logging para dejar evidencia. Antes de empezar, conviene revisar cómo pathlib administra archivos y rutas.
Definir origen y destino
El origen es la carpeta que deseas proteger. El destino debe estar en otra ubicación, idealmente otro disco, servidor o servicio sincronizado. Guardar el backup en la misma unidad protege contra errores de edición, pero no contra una avería física.
from pathlib import Path
ORIGEN = Path.home() / "Documentos" / "proyecto"
DESTINO = Path.home() / "Backups" / "proyecto"
if not ORIGEN.exists():
raise FileNotFoundError(f"No existe el origen: {ORIGEN}")
DESTINO.mkdir(parents=True, exist_ok=True)mkdir(parents=True, exist_ok=True) crea toda la estructura necesaria sin fallar cuando la carpeta ya existe. Evita escribir rutas con separadores manuales; Path mantiene el script compatible entre sistemas.
Crear una copia versionada
Una copia versionada crea una carpeta nueva para cada ejecución. Así puedes recuperar un estado anterior en lugar de conservar únicamente la última versión.
from datetime import datetime
from pathlib import Path
import shutil
ORIGEN = Path.home() / "Documentos" / "proyecto"
BASE_BACKUPS = Path.home() / "Backups"
marca = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
destino = BASE_BACKUPS / f"proyecto_{marca}"
shutil.copytree(ORIGEN, destino)
print(f"Backup creado en {destino}")copytree() copia directorios completos. Si el destino ya existe, por defecto genera un error, lo cual es útil porque evita mezclar accidentalmente dos ejecuciones. La documentación de shutil detalla las diferencias entre copy, copy2, copytree y move.
Copiar metadatos y enlaces
shutil.copy2() intenta conservar metadatos como la fecha de modificación. Para árboles completos, copytree() utiliza normalmente una función de copia compatible con ese comportamiento. Los enlaces simbólicos requieren una decisión explícita: puedes copiar el enlace o seguirlo y copiar el contenido apuntado.
shutil.copytree(
ORIGEN,
destino,
symlinks=True,
ignore_dangling_symlinks=True,
)La mejor opción depende de la restauración esperada. En un backup personal, seguir enlaces puede duplicar grandes cantidades de datos. En una configuración técnica, conservarlos puede ser necesario.
Excluir archivos innecesarios
No siempre conviene copiar cachés, entornos virtuales, carpetas de compilación o repositorios temporales.
patrones_ignorados = shutil.ignore_patterns(
"__pycache__",
"*.pyc",
".venv",
"node_modules",
".git",
)
shutil.copytree(
ORIGEN,
destino,
ignore=patrones_ignorados,
)Excluye solo elementos que puedas reconstruir. Nunca ignores una base de datos o archivo de configuración importante sin una estrategia alternativa.
Añadir registros del proceso
Un script automático debe indicar qué ocurrió incluso cuando nadie estaba mirando la terminal.
import logging
logging.basicConfig(
filename="backup.log",
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(message)s",
)
try:
shutil.copytree(ORIGEN, destino, ignore=patrones_ignorados)
except Exception:
logging.exception("El backup falló")
raise
else:
logging.info("Backup creado: %s", destino)Para configurar rotación y distintos destinos, consulta la guía de logging en Python. No registres secretos, contraseñas o contenido privado.
Verificar la copia
Que la función termine sin excepción no garantiza por sí solo que la copia sea restaurable. Una verificación básica compara la cantidad y el tamaño de los archivos.
def inventario(carpeta: Path) -> dict[str, int]:
datos = {}
for ruta in carpeta.rglob("*"):
if ruta.is_file():
relativa = ruta.relative_to(carpeta).as_posix()
datos[relativa] = ruta.stat().st_size
return datos
original = inventario(ORIGEN)
copia = inventario(destino)
if original != copia:
raise RuntimeError("La verificación del backup falló")Para archivos críticos puedes calcular hashes, aunque esto aumenta el tiempo de ejecución. También debes probar restauraciones periódicamente; un backup que nunca se ha restaurado es una suposición, no una garantía.
Calcular hashes de archivos críticos
import hashlib
def sha256(ruta: Path) -> str:
resumen = hashlib.sha256()
with ruta.open("rb") as archivo:
while bloque := archivo.read(1024 * 1024):
resumen.update(bloque)
return resumen.hexdigest()Procesar en bloques evita cargar archivos enormes en memoria. Puedes comparar el hash del original y la copia para documentos especialmente importantes.
Eliminar versiones antiguas
Las copias versionadas consumen espacio. Una política sencilla conserva las últimas siete ejecuciones.
backups = sorted(
BASE_BACKUPS.glob("proyecto_*"),
key=lambda ruta: ruta.stat().st_mtime,
reverse=True,
)
for antiguo in backups[7:]:
shutil.rmtree(antiguo)
logging.info("Backup antiguo eliminado: %s", antiguo)Antes de eliminar, verifica que el patrón sea específico y que la carpeta base sea realmente la esperada. rmtree() borra recursivamente y no dispone de una papelera automática.
Programar la ejecución
En Windows puedes usar el Programador de tareas o iniciar una utilidad al arrancar. La guía para ejecutar scripts en el inicio de Windows explica varias opciones. En Linux y macOS puedes utilizar cron, systemd timers o launchd. Programa el proceso en un horario en el que el origen esté disponible y evita ejecutar varias instancias simultáneamente.
Código completo simplificado
from datetime import datetime
from pathlib import Path
import logging
import shutil
ORIGEN = Path.home() / "Documentos" / "proyecto"
BASE = Path.home() / "Backups"
logging.basicConfig(
filename=BASE / "backup.log",
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(message)s",
)
def crear_backup() -> Path:
if not ORIGEN.is_dir():
raise FileNotFoundError(ORIGEN)
BASE.mkdir(parents=True, exist_ok=True)
marca = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
destino = BASE / f"proyecto_{marca}"
ignorar = shutil.ignore_patterns("__pycache__", "*.pyc", ".venv")
shutil.copytree(ORIGEN, destino, ignore=ignorar)
logging.info("Backup creado en %s", destino)
return destino
if __name__ == "__main__":
try:
ruta = crear_backup()
except Exception:
logging.exception("No fue posible crear el backup")
raise
else:
print(f"Copia creada: {ruta}")Estrategia 3-2-1
Una práctica ampliamente utilizada consiste en mantener tres copias de los datos, en dos tipos de almacenamiento y una copia fuera del lugar principal. El script puede ser una pieza de esa estrategia, pero no reemplaza por sí solo el almacenamiento externo, el cifrado, el control de acceso ni la supervisión.
La guía de automatización de tareas con Python ofrece ideas para convertir el proceso en una herramienta más amplia. También puedes revisar la documentación oficial de pathlib para manejar rutas, permisos y recorridos de manera segura.
Conclusión
Una copia de seguridad automática debe ser repetible, verificable y fácil de restaurar. Python proporciona todas las piezas para copiar carpetas, crear versiones, excluir archivos reconstruibles, registrar errores y aplicar retención. Empieza con una carpeta de prueba, simula una restauración y luego adapta la solución a tus datos reales. La automatización reduce el trabajo manual, pero la confianza aparece únicamente cuando compruebas que la copia conserva exactamente lo que necesitas.






