El gestor ya funciona mientras está abierto. Ahora conservará las tareas entre sesiones y responderá con claridad a los problemas de lectura o escritura. Añadirás un módulo de almacenamiento y sustituirás la función principal. Mantendrás la lógica y las funciones de presentación que ya comprobaste. Trabaja primero con tareas inventadas y conserva una copia de la versión anterior.
Crear el módulo de almacenamiento
Guarda el siguiente código completo como almacenamiento.py junto a app.py y logica.py. La validación comprueba que cada registro cumple el modelo. cargar devuelve una lista vacía únicamente cuando el archivo no existe. Un documento mal formado o con estructura incorrecta provoca un error que el programa principal tratará sin sobrescribirlo.
import json
def datos_validos(datos):
if not isinstance(datos, list):
return False
for tarea in datos:
if not isinstance(tarea, dict):
return False
if set(tarea) != {"titulo", "hecha"}:
return False
if not isinstance(tarea["titulo"], str):
return False
if not tarea["titulo"].strip():
return False
if type(tarea["hecha"]) is not bool:
return False
return True
def cargar(ruta):
try:
texto = ruta.read_text(encoding="utf-8")
except FileNotFoundError:
return []
datos = json.loads(texto)
if not datos_validos(datos):
raise ValueError("El archivo no contiene tareas válidas")
return datos
def guardar(ruta, tareas):
if not datos_validos(tareas):
raise ValueError("Los datos no cumplen el modelo")
texto = json.dumps(tareas, ensure_ascii=False, indent=2)
temporal = ruta.with_name(ruta.name + ".tmp")
temporal.write_text(texto, encoding="utf-8")
temporal.replace(ruta)
raise crea un error explícito cuando los datos no cumplen la estructura. json.JSONDecodeError pertenece a la familia ValueError, por lo que podremos tratar juntos el formato JSON inválido y el modelo inválido al cargar. Los problemas del sistema de archivos se comunicarán mediante OSError o sus subclases.
El guardado escribe primero un archivo temporal en la misma carpeta y después reemplaza el destino. Así evita vaciar directamente el archivo anterior antes de terminar la escritura del nuevo. Este patrón no es una copia de seguridad ni garantiza recuperación ante todos los fallos del dispositivo. La aplicación está pensada para una única instancia local.
Actualizar app.py sin duplicar funciones
Conserva la importación de logica y las funciones listar_tareas y solicitar_completado de la lección anterior. Añade estas importaciones y la constante RUTA al principio del archivo. No cambies el nombre de los tres módulos.
from pathlib import Path
from almacenamiento import cargar, guardar
RUTA = Path(__file__).resolve().parent / "tareas.json"
Sustituye por completo la antigua función main por la que aparece a continuación. Mantén al final del archivo una sola condición if __name__ == «__main__»: con su llamada a main. No conserves dos definiciones de main ni dos bloques de arranque.
def main():
try:
tareas = cargar(RUTA)
except (OSError, ValueError, UnicodeDecodeError) as error:
print(f"No se han cargado los datos: {error}")
print("Revisa una copia del archivo. No se ha sobrescrito")
return
print(f"Archivo de datos: {RUTA}")
while True:
print("1: añadir | 2: listar | 3: completar")
print("4: guardar | 0: guardar y salir | 5: salir sin guardar")
opcion = input("Opción: ").strip()
if opcion == "1":
titulo = input("Título de la tarea: ")
if agregar_tarea(tareas, titulo):
print("Tarea añadida. Pendiente de guardar")
else:
print("El título no puede estar vacío")
elif opcion == "2":
listar_tareas(tareas)
elif opcion == "3":
solicitar_completado(tareas)
elif opcion in ("4", "0"):
try:
guardar(RUTA, tareas)
except (OSError, ValueError) as error:
print(f"No se ha podido guardar: {error}")
else:
print("Datos guardados")
if opcion == "0":
break
elif opcion == "5":
respuesta = input("Perderás cambios no guardados. ¿Salir? s/n: ")
if respuesta.strip().lower() == "s":
break
else:
print("Opción no válida")
El bloque de guardado muestra «Datos guardados» solo si la operación termina correctamente. Si falla, el bucle continúa. La opción cero no cierra después de un fallo de escritura. Puedes revisar el problema e intentar guardar otra vez o elegir conscientemente salir sin guardar.
Comprobar la continuidad entre sesiones
Añade dos tareas, completa una y elige cuatro. Abre tareas.json en un editor para inspeccionarlo sin modificarlo. Debe contener ambos títulos y sus estados. Después sal con cero, vuelve a ejecutar y utiliza el listado. Los datos deben coincidir con lo que guardaste.
Haz otra prueba añadiendo una tercera tarea y saliendo mediante cinco con confirmación s. Al abrir de nuevo esa tercera tarea no debe aparecer. La última versión guardada permanece en el archivo. Esta diferencia enseña a separar el estado actual en memoria del estado persistido.
Revisar un archivo dañado sin perderlo
Cierra la aplicación y copia tareas.json con otro nombre. En la versión de prueba elimina un carácter necesario para romper la sintaxis. Al arrancar debe aparecer el aviso de carga y terminar sin mostrar el menú. Comprueba que los bytes del archivo dañado no han sido reemplazados por una lista vacía.
Restaura la copia correcta antes de continuar. No borres el archivo problemático como primera reacción si contiene datos que quieres recuperar. La aplicación no incluye una herramienta de reparación. Su responsabilidad aquí consiste en explicar que no puede interpretarlo y evitar un guardado automático que agrave la situación.
Límites de esta versión
El programa guarda cuando eliges cuatro o cero. Si cierras la terminal a la fuerza puedes perder cambios posteriores al último guardado. Tampoco coordina dos ejecuciones simultáneas: la última que guarde puede sustituir cambios de la otra. Estas limitaciones son aceptables para la práctica siempre que las conozcas y no prometas funciones que no están implementadas.
Errores habituales
- Añadir la nueva función main sin retirar la anterior.
- Tratar cualquier fallo de carga como una lista vacía.
- Mostrar éxito antes de terminar la escritura.
- Confundir el archivo temporal con una copia de seguridad histórica.
Lo que deberías recordar
- La carga distingue ausencia de archivo y contenido inválido.
- Guardar en disco y modificar en memoria son operaciones diferentes.
- Un mensaje de éxito debe corresponder a una operación completada.
- La recuperación necesita conservar el archivo que ha dado problemas.
Ejercicio práctico
Completa la decimonovena hoja con la ruta real, las dos tareas guardadas y el resultado de reabrir. Prueba la salida sin guardar y la carga de una copia dañada. Anota qué permaneció en el archivo en cada caso. Termina restaurando los datos válidos y ejecutando otra vez el listado para dejar el proyecto en un estado utilizable.
Siguiente capítulo: Terminar el proyecto y seguir aprendiendo.








