Una carpeta de Descargas con cientos de documentos, imágenes, instaladores y archivos comprimidos se vuelve difícil de mantener. Python puede clasificar esos elementos automáticamente según su extensión, fecha, tamaño o nombre. Con una estrategia segura, el script crea carpetas, mueve cada archivo a su destino y registra todas las acciones para que puedas revisar lo ocurrido.
Este proyecto combina varias habilidades importantes: trabajar con rutas mediante pathlib, manipular archivos con el módulo os, crear automatizaciones cotidianas y preparar scripts que se ejecutan al iniciar Windows. Antes de mover datos reales, prueba siempre con una carpeta temporal.
Planificar las categorías
El primer paso es definir qué extensiones pertenecen a cada carpeta. Un diccionario hace que la configuración sea legible:
CATEGORIAS = {
"Imagenes": {".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"},
"Documentos": {".pdf", ".doc", ".docx", ".txt", ".odt"},
"Hojas_calculo": {".csv", ".xls", ".xlsx", ".ods"},
"Comprimidos": {".zip", ".rar", ".7z", ".tar", ".gz"},
"Audio": {".mp3", ".wav", ".flac", ".m4a"},
"Video": {".mp4", ".mkv", ".avi", ".mov"},
"Instaladores": {".exe", ".msi", ".dmg", ".deb"},
}Las extensiones se guardan en minúsculas. Al analizar cada archivo también convertiremos su sufijo a minúsculas, de modo que FOTO.JPG y foto.jpg sean tratados de la misma manera.
Elegir la carpeta de origen
from pathlib import Path
ORIGEN = Path.home() / "Downloads"
if not ORIGEN.exists():
raise FileNotFoundError(f"No existe la carpeta: {ORIGEN}")En algunos sistemas en español la carpeta puede mostrarse como “Descargas”, pero su ruta interna seguir siendo Downloads. Comprueba el valor con print(ORIGEN). También puedes recibir la ruta desde un argumento de terminal o una variable de entorno.
Buscar la categoría de un archivo
def obtener_categoria(archivo: Path) -> str:
extension = archivo.suffix.lower()
for categoria, extensiones in CATEGORIAS.items():
if extension in extensiones:
return categoria
return "Otros"Path.suffix devuelve únicamente la última extensión. Para archivos como copia.tar.gz, quizá quieras analizar archivo.suffixes. En la mayoría de los casos, clasificar por .gz es suficiente.
Evitar sobrescribir archivos
Si la carpeta de destino ya contiene informe.pdf, mover otro archivo con el mismo nombre puede sobrescribirlo o provocar un error. Esta función crea un nombre disponible:
def nombre_disponible(destino: Path) -> Path:
if not destino.exists():
return destino
contador = 1
while True:
candidato = destino.with_name(
f"{destino.stem}_{contador}{destino.suffix}"
)
if not candidato.exists():
return candidato
contador += 1Así se generan nombres como informe_1.pdf, informe_2.pdf y sucesivos. En proyectos empresariales, también puedes añadir una fecha o un identificador único.
Mover los archivos con seguridad
import shutil
def organizar_archivo(archivo: Path, simulacion: bool = True) -> Path:
categoria = obtener_categoria(archivo)
carpeta_destino = ORIGEN / categoria
destino = nombre_disponible(carpeta_destino / archivo.name)
if simulacion:
print(f"[SIMULACION] {archivo.name} -> {destino}")
return destino
carpeta_destino.mkdir(parents=True, exist_ok=True)
shutil.move(str(archivo), str(destino))
return destinoEl parámetro simulacion es una protección importante. Permite revisar el plan sin modificar nada. La documentación oficial de pathlib explica cómo representar rutas de manera portable, mientras que la documentación oficial de shutil describe las operaciones de copia y movimiento.
Recorrer únicamente los archivos adecuados
def organizar_carpeta(simulacion: bool = True) -> None:
for elemento in ORIGEN.iterdir():
if not elemento.is_file():
continue
if elemento.name.startswith("."):
continue
organizar_archivo(elemento, simulacion=simulacion)No recorremos subcarpetas porque podríamos reorganizar estructuras que el usuario ya preparó. Tampoco movemos archivos ocultos. Puedes añadir exclusiones para el propio script, archivos temporales o descargas parciales como .crdownload y .part.
Ignorar archivos que todavía se están descargando
EXTENSIONES_TEMPORALES = {".crdownload", ".part", ".tmp"}
if elemento.suffix.lower() in EXTENSIONES_TEMPORALES:
continueOtra estrategia consiste en comprobar que el tamaño no cambie durante algunos segundos. Esto es útil cuando el navegador utiliza el nombre final antes de terminar la descarga:
import time
def esta_estable(archivo: Path, espera: int = 3) -> bool:
inicial = archivo.stat().st_size
time.sleep(espera)
final = archivo.stat().st_size
return inicial == finalNo bloquees el programa demasiado tiempo si hay muchos archivos. Para vigilancia continua, una biblioteca como Watchdog puede reaccionar a eventos del sistema de archivos.
Registrar todas las acciones
import logging
logging.basicConfig(
filename=ORIGEN / "organizador.log",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
encoding="utf-8"
)Después de mover:
logging.info("Movido: %s -> %s", archivo, destino)Y si ocurre un error:
try:
destino = organizar_archivo(elemento, simulacion=False)
except (PermissionError, OSError) as error:
logging.exception("No se pudo mover %s: %s", elemento, error)El registro permite investigar problemas y, si guardas origen y destino, construir una función para deshacer movimientos.
Crear un modo de deshacer
Una solución sencilla es escribir cada movimiento en CSV:
import csv
from datetime import datetime
def registrar_movimiento(origen: Path, destino: Path) -> None:
historial = ORIGEN / "historial_movimientos.csv"
nuevo = not historial.exists()
with historial.open("a", newline="", encoding="utf-8") as archivo:
escritor = csv.writer(archivo)
if nuevo:
escritor.writerow(["fecha", "origen", "destino"])
escritor.writerow([
datetime.now().isoformat(timespec="seconds"),
str(origen),
str(destino),
])Para deshacer, lee las filas en orden inverso y mueve cada destino de vuelta a su origen, siempre que el archivo exista y el nombre original esté libre.
Organizar por año y mes
Las fotografías pueden ordenarse por fecha de modificación:
from datetime import datetime
fecha = datetime.fromtimestamp(archivo.stat().st_mtime)
carpeta = ORIGEN / "Imagenes" / str(fecha.year) / f"{fecha.month:02d}"Ten en cuenta que la fecha de modificación no siempre representa la fecha de captura. Para fotografías, los metadatos EXIF pueden ser más precisos. Sin embargo, deben validarse porque pueden estar ausentes o dañados.
Script completo simplificado
from pathlib import Path
import logging
import shutil
ORIGEN = Path.home() / "Downloads"
CATEGORIAS = {
"Imagenes": {".jpg", ".jpeg", ".png", ".webp"},
"Documentos": {".pdf", ".docx", ".txt"},
"Comprimidos": {".zip", ".rar", ".7z"},
}
logging.basicConfig(
filename=ORIGEN / "organizador.log",
level=logging.INFO,
encoding="utf-8"
)
def categoria(archivo):
extension = archivo.suffix.lower()
for nombre, extensiones in CATEGORIAS.items():
if extension in extensiones:
return nombre
return "Otros"
def destino_libre(destino):
contador = 1
candidato = destino
while candidato.exists():
candidato = destino.with_name(
f"{destino.stem}_{contador}{destino.suffix}"
)
contador += 1
return candidato
def main():
for archivo in ORIGEN.iterdir():
if not archivo.is_file() or archivo.name.startswith("."):
continue
if archivo.suffix.lower() in {".part", ".tmp", ".crdownload"}:
continue
carpeta = ORIGEN / categoria(archivo)
carpeta.mkdir(exist_ok=True)
destino = destino_libre(carpeta / archivo.name)
try:
shutil.move(str(archivo), str(destino))
logging.info("%s -> %s", archivo, destino)
except OSError:
logging.exception("Error moviendo %s", archivo)
if __name__ == "__main__":
main()Programar la ejecución
En Windows puedes utilizar el Programador de tareas para ejecutarlo al iniciar sesión o cada hora. En Linux y macOS puedes utilizar cron o un servicio del sistema. No programes la tarea hasta haber probado el modo simulación, revisado las exclusiones y creado una copia de seguridad.
Buenas prácticas
No muevas archivos del sistema ni carpetas completas sin una regla explícita. Mantén una lista de exclusión, registra los cambios, evita sobrescrituras y prueba con copias. Si el script se ejecuta continuamente, impide que procese su propio archivo de log e historial. Añade límites para que un error de configuración no mueva miles de archivos fuera de la ubicación esperada.
Conclusión
Python permite convertir una carpeta caótica en una estructura ordenada con pocas funciones. pathlib representa las rutas, shutil.move realiza los movimientos y logging conserva un historial. La diferencia entre un experimento y una automatización confiable está en el modo simulación, la prevención de sobrescrituras, las exclusiones y la posibilidad de deshacer. Con esas protecciones, el organizador puede ejecutarse de forma periódica y ahorrar trabajo manual sin poner tus datos en riesgo.






