Python en la vida real
Hasta aquí el manual ha ido presentando piezas sueltas: tipos, bucles, funciones, archivos, excepciones. Cada una con ejemplos que caben en tres líneas y que existen solo para explicar esa pieza. Esta última lección hace lo contrario: tres programas completos, cortos pero enteros, del tipo que uno escribe para resolverse un problema propio.
No hay nada en ellos que no hayas visto ya. Esa es justamente la idea: comprobar que con lo aprendido hasta aquí ya se pueden escribir cosas útiles, y ver qué aspecto tiene el código cuando deja de ser un ejemplo y pasa a ser una herramienta.
1. Ordenar una carpeta de descargas
El problema es universal: la carpeta de descargas acumula imágenes, PDF, comprimidos y basura variada hasta que no hay quien encuentre nada. Ordenarla a mano es aburrido y hay que repetirlo cada pocas semanas, que son las dos señales de que conviene escribir un script.
from pathlib import Path
CARPETA = Path.home() / "Descargas"
DESTINOS = {
".jpg": "imagenes", ".png": "imagenes", ".webp": "imagenes",
".pdf": "documentos", ".odt": "documentos", ".txt": "documentos",
".zip": "comprimidos", ".tar": "comprimidos", ".gz": "comprimidos",
}
for archivo in CARPETA.iterdir():
if not archivo.is_file():
continue
destino = DESTINOS.get(archivo.suffix.lower())
if destino is None:
continue
carpeta = CARPETA / destino
carpeta.mkdir(exist_ok=True)
archivo.rename(carpeta / archivo.name)
print(f"{archivo.name} -> {destino}/")
Hay tres decisiones ahí que merece la pena señalar.
La primera es el diccionario DESTINOS. Podría haberse escrito como una cadena de if y elif, pero entonces añadir una extensión nueva obligaría a tocar la lógica del programa. Así solo se añade una línea a la tabla, y la lógica no se toca nunca. Es un patrón que reaparece constantemente: cuando un if empieza a crecer en ramas que solo se diferencian en un valor, casi siempre quiere ser un diccionario.
La segunda es DESTINOS.get(...) en lugar de DESTINOS[...]. Con los corchetes, un archivo de una extensión desconocida lanzaría KeyError y detendría el script. Con .get() devuelve None, que aquí significa exactamente "este archivo no me interesa, pasa al siguiente". El error previsible se convierte en parte del flujo normal.
La tercera es .suffix.lower(). Un archivo descargado puede llamarse foto.JPG con la extensión en mayúsculas, y sin ese .lower() el script lo ignoraría sin decir nada. Son los detalles que separan un ejemplo de manual de algo que funciona con datos reales.
Nota: este script mueve archivos, así que no es inofensivo. rename() sobrescribe sin avisar si en el destino ya existe un archivo con el mismo nombre. Antes de lanzar por primera vez un programa que modifica cosas, conviene ejecutarlo con el rename() comentado y solo el print() activo, para ver qué haría sin que lo haga. Es lo que se conoce como un ensayo en seco.
2. Comprobar los enlaces de un sitio web
Muchos sitios hechos a mano guardan su índice en un archivo aparte: una lista de qué páginas existen, cómo se titulan y en qué orden van. El menú de navegación se construye leyendo ese archivo, en lugar de repetirlo a mano en cada página.
El problema evidente es que el índice y el disco pueden dejar de coincidir. Se renombra una página y el índice apunta a un archivo que ya no está. O se escribe una página nueva y se olvida añadirla al índice, con lo que queda publicada pero inalcanzable. Los dos fallos son molestos, ninguno salta a la vista y ambos se detectan mecánicamente. Trabajo de máquina.
Supongamos un indice.json como este, con dos secciones:
{
"cocina": {
"1": ["index.html", "Empezar por aquí"],
"2": ["masas.html", "Masas y fermentados"],
"3": [
["tablas.html", "Tablas de equivalencias"],
["glosario.html", "Glosario"]
]
},
"huerto": {
"1": ["index.html", "Empezar por aquí"],
"2": ["riego.html", "Sistemas de riego"]
}
}
Fíjate en la entrada "3" de cocina: no es una página, sino un grupo de dos apéndices. Es una irregularidad muy típica de los archivos de configuración que han ido creciendo con el tiempo, y habrá que tenerla en cuenta.
"""Comprueba que el índice de un sitio y los archivos en disco coinciden."""
import json
from pathlib import Path
CONTENIDO = Path("contenido")
def archivos_de_seccion(paginas):
"""Devuelve los nombres de archivo de una sección del índice.
El JSON no es uniforme: casi todas las entradas son
[archivo, titulo], pero los apéndices van agrupados como una
lista de esas listas. Hay que contemplar los dos casos.
"""
archivos = []
for entrada in paginas.values():
if isinstance(entrada[0], list):
archivos += [sub[0] for sub in entrada]
else:
archivos.append(entrada[0])
return archivos
indice = json.loads((CONTENIDO / "indice.json").read_text(encoding="utf-8"))
# 1) Enlaces del índice que no tienen archivo detrás.
rotos = 0
for seccion, paginas in indice.items():
for archivo in archivos_de_seccion(paginas):
if not (CONTENIDO / seccion / archivo).exists():
print(f" FALTA {seccion}/{archivo}")
rotos += 1
print(f"{len(indice)} secciones revisadas. Enlaces rotos: {rotos}.")
# 2) Al revés: archivos que existen pero nadie enlaza.
print("\nHuérfanos (existen pero no salen en el índice):")
for seccion, paginas in indice.items():
enlazados = set(archivos_de_seccion(paginas))
for ruta in sorted((CONTENIDO / seccion).glob("*.html")):
if ruta.name not in enlazados:
print(f" {seccion}/{ruta.name}")
Sobre una carpeta donde falta masas.html y sobra un compost.html que nadie enlaza, la salida es:
FALTA cocina/masas.html
2 secciones revisadas. Enlaces rotos: 1.
Huérfanos (existen pero no salen en el índice):
huerto/compost.html
Lo interesante de este programa no es ninguna técnica nueva, sino que comprueba el dato en las dos direcciones. Un enlace roto y un archivo huérfano son el mismo desajuste visto desde cada lado, y casi todo el mundo escribe solo la primera mitad. La segunda es la que suele encontrar cosas, porque un archivo huérfano no da ningún error: la página existe, se sirve perfectamente y sencillamente no hay forma de llegar a ella navegando. Puede pasar años así.
La función archivos_de_seccion() existe por una razón muy corriente: los datos reales casi nunca son uniformes. Encerrar esa irregularidad dentro de una función tiene dos ventajas: el bucle principal se lee limpio, y cuando el formato del índice cambie habrá exactamente un sitio que tocar. Ese es el motivo más frecuente para escribir una función, y no el que suele darse: no es reutilizar código, es esconder una complicación.
Fíjate también en enlazados = set(...). Podría ser una lista y el programa daría el mismo resultado. Se usa un conjunto porque la comprobación ruta.name not in enlazados se ejecuta una vez por cada archivo, y buscar en un conjunto es inmediato mientras que buscar en una lista obliga a recorrerla entera. Con diez páginas da exactamente igual; el hábito es lo que importa.
3. Un informe a partir de un CSV
El tercer caso es el más común de todos: alguien te pasa un archivo con datos y necesitas un resumen. Un extracto bancario, un registro de ventas, una hoja de gastos exportada desde el móvil. Supongamos un gastos.csv así:
fecha,categoria,concepto,importe
2026-09-01,hosting,alojamiento anual,142.80
2026-09-03,comida,compra semanal,64.20
2026-09-05,hosting,renovación del dominio,14.99
2026-09-07,transporte,abono mensual,32.00
Y la pregunta: cuánto se ha gastado en cada categoría, de mayor a menor.
import csv
totales = {}
with open("gastos.csv", encoding="utf-8") as f:
for fila in csv.DictReader(f):
categoria = fila["categoria"]
importe = float(fila["importe"])
totales[categoria] = totales.get(categoria, 0) + importe
for categoria, total in sorted(totales.items(), key=lambda par: par[1], reverse=True):
print(f"{categoria}: {total:.2f} euros")
print(f"Total del mes: {sum(totales.values()):.2f} euros")
Salida:
hosting: 157.79 euros
comida: 64.20 euros
transporte: 32.00 euros
Total del mes: 253.99 euros
La línea que concentra toda la idea es totales[categoria] = totales.get(categoria, 0) + importe. El segundo argumento de .get() es el valor a devolver cuando la clave todavía no existe, así que la primera vez que aparece una categoría se suma sobre cero y a partir de ahí sobre lo acumulado. Sin ese truco harían falta un if y dos ramas para distinguir la primera aparición de las siguientes. Acumular en un diccionario es probablemente el patrón más útil de todo el lenguaje para tratar datos.
El key=lambda par: par[1] le dice a sorted() que ordene por el segundo elemento de cada pareja, es decir por el importe y no por el nombre de la categoría. Y encoding="utf-8" está ahí porque sin él Python usa la codificación por defecto del sistema, que en Windows no es UTF-8 y convierte cualquier acento en un error o en un símbolo raro.
Qué tienen en común
Los tres programas ocupan menos de veinticinco líneas y comparten una forma que se repite en casi cualquier script de este tipo:
- Leer del mundo, no del teclado. Ninguno usa
input(). Los datos vienen del sistema de archivos, de un JSON o de un CSV. Un script que hay que atender mientras se ejecuta no ahorra tanto trabajo como uno que se lanza y ya está. - Las rutas, al principio y en mayúsculas.
CARPETA,DESTINOS,CONTENIDO. Es la convención de PEP8 para las constantes, y sirve para que al llevarte el script a otro equipo sepas qué hay que cambiar sin leértelo entero. - Un informe por pantalla. Todos terminan contando qué han hecho o qué han encontrado. Un script silencioso es un script en el que no se puede confiar.
- Leer es seguro, escribir no. El segundo y el tercero solo miran, así que se pueden ejecutar mil veces sin consecuencias. El primero mueve archivos, y por eso es el único que necesita precauciones.
Si algo vale la pena llevarse de este manual, es que la barrera para automatizar una tarea propia es mucho más baja de lo que parece desde fuera. Ninguno de estos tres programas necesita clases, ni bibliotecas externas, ni nada que no esté en las trece lecciones anteriores.
En la próxima lección: los apéndices del manual: la guía de estilo PEP8, los entornos virtuales, el manejo de paquetes con pip y JupyterLab.