WhatsApp

  

17 Guardando datos de tu GUI: archivos, JSON y SQLite

Aprende por qué y cómo persistir la configuración, listas y registros de tus aplicaciones gráficas usando archivos, JSON y bases de datos SQLite. Incluye ejemplos completos y buenas prácticas.

Guardando datos de tu GUI: archivos, JSON y SQLite

Persistir la información de una aplicación gráfica (configuración, listas, registros) es clave para una buena experiencia de usuario y para evitar pérdida de datos. En este artículo analizamos tres enfoques ligeros – archivos planos, JSON y SQLite – y presentamos un caso práctico: una lista de tareas que guarda su estado al cerrar y lo recupera al abrir.


1. ¿Por qué es útil guardar datos de la aplicación?

  • Experiencia de usuario consistente: la aplicación recuerda la última configuración (tema, idioma, posición de ventanas).
  • Prevención de pérdida de información: listas de tareas, notas o históricos no desaparecen al cerrar la app.
  • Facilita la interoperabilidad: archivos exportables pueden ser importados por otras herramientas o versiones de la misma app.
  • Escalabilidad gradual: se empieza con un archivo sencillo y, cuando crecen los requerimientos, se migra a una base de datos ligera.

2. Opciones de persistencia: archivo plano, JSON y SQLite

Archivo plano (texto, CSV)

Ideal para datos tabulares simples o logs. No requiere dependencias, pero carece de estructura y validación.

  • Ventajas: ultra‑ligero, legible por cualquier editor.
  • Desventajas: difícil de mantener cuando el esquema cambia, sin tipos de datos.

JSON

Formato de texto estructurado que representa objetos y arrays. Muy usado en APIs y configuraciones.

  • Ventajas: legible, soportado nativamente por la mayoría de lenguajes, permite anidamiento.
  • Desventajas: sin transacciones ni índices; el archivo crece linealmente.

SQLite

Motor de base de datos SQL embebido, almacena todo en un único archivo .sqlite. Proporciona ACID, índices, consultas avanzadas y es extremadamente portátil.

  • Ventajas: transacciones, consultas rápidas, capacidad de crecer a GBs, soporte de índices.
  • Desventajas: curva de aprendizaje de SQL (aunque básica), archivo más grande que JSON para datos muy pequeños.

3. Comparativa rápida (dos columnas)

Características
  • Facilidad de uso
  • Soporte de transacciones
  • Índices y búsquedas
  • Tamaño del archivo
  • Portabilidad
Archivo plano / CSV
  • Muy fácil
  • No
  • No
  • Pequeño
  • Altísima
JSON
  • Fácil
  • No
  • No (búsqueda lineal)
  • Moderado
  • Muy alta
SQLite
  • Fácil (con sqlite3)
  • Variable (GBs)
  • Excelente

4. JSON: cómo guardar y leer datos

En Python (pero la lógica es idéntica en JavaScript, C#, Java, etc.) usamos el módulo json estándar.

import json, pathlib
DATA_FILE = pathlib.Path('config.json')
# --- Estructura de datos de ejemplo ---
config = {
    "theme": "dark",
    "language": "es",
    "window": {"width": 800, "height": 600}
}
# Guardar (serializar)
with DATA_FILE.open('w', encoding='utf-8') as f:
    json.dump(config, f, indent=4, ensure_ascii=False)
# Leer (deserializar) con manejo de errores
try:
    with DATA_FILE.open('r', encoding='utf-8') as f:
        loaded = json.load(f)
except FileNotFoundError:
    loaded = {}  # valores por defecto
except json.JSONDecodeError as e:
    print('⚠️ Archivo corrupto:', e)
    loaded = {}

Buenas prácticas al trabajar con JSON:

  • Usar indent para que el archivo sea legible y fácil de versionar.
  • Validar la estructura con jsonschema o pydantic si el proyecto es grande.
  • No almacenar datos sensibles sin cifrado (contraseñas, tokens).
  • Siempre envolver la carga en try/except para evitar que una corrupción bloquee la aplicación.

5. Caso práctico: aplicación de lista de tareas (JSON)

Imagina una GUI sencilla con Tkinter. Cada tarea es un diccionario con id, texto y completado. Guardamos el listado completo en tasks.json al cerrar y lo cargamos al iniciar.

import json, pathlib, tkinter as tk, uuid
TASK_FILE = pathlib.Path('tasks.json')
class TodoApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title('🗒️ Lista de tareas')
        self.geometry('400x500')
        self.tasks = []
        self._load_tasks()
        self._build_ui()
        self.protocol('WM_DELETE_WINDOW', self._on_close)
    def _build_ui(self):
        self.entry = tk.Entry(self, font=('Helvetica', 12))
        self.entry.pack(fill='x', padx=10, pady=5)
        self.entry.bind('', lambda e: self._add_task())
        self.listbox = tk.Listbox(self, font=('Helvetica', 12))
        self.listbox.pack(expand=True, fill='both', padx=10, pady=5)
        self._refresh_listbox()
        btn_frame = tk.Frame(self)
        btn_frame.pack(pady=5)
        tk.Button(btn_frame, text='✔ Completar', command=self._toggle_done).pack(side='left', padx=5)
        tk.Button(btn_frame, text='🗑️ Borrar', command=self._delete_task).pack(side='left', padx=5)
    def _add_task(self):
        text = self.entry.get().strip()
        if text:
            self.tasks.append({"id": str(uuid.uuid4()), "texto": text, "completado": False})
            self.entry.delete(0, tk.END)
            self._refresh_listbox()
    def _toggle_done(self):
        sel = self.listbox.curselection()
        if sel:
            idx = sel[0]
            self.tasks[idx]["completado"] = not self.tasks[idx]["completado"]
            self._refresh_listbox()
    def _delete_task(self):
        sel = self.listbox.curselection()
        if sel:
            idx = sel[0]
            del self.tasks[idx]
            self._refresh_listbox()
    def _refresh_listbox(self):
        self.listbox.delete(0, tk.END)
        for t in self.tasks:
            prefix = '✅' if t['completado'] else '❌'
            self.listbox.insert(tk.END, f"{prefix} {t['texto']}")
    def _load_tasks(self):
        try:
            with TASK_FILE.open('r', encoding='utf-8') as f:
                self.tasks = json.load(f)
        except (FileNotFoundError, json.JSONDecodeError):
            self.tasks = []
    def _on_close(self):
        # Guardar antes de salir
        with TASK_FILE.open('w', encoding='utf-8') as f:
            json.dump(self.tasks, f, indent=2, ensure_ascii=False)
        self.destroy()
if __name__ == '__main__':
    app = TodoApp()
    app.mainloop()

Este ejemplo muestra:

  • Carga automática al iniciar (_load_tasks).
  • Persistencia al cerrar (_on_close).
  • Uso de uuid4 para IDs únicos (evita colisiones si se sincroniza con la nube).

Tips de seguridad:

  • Si el archivo contiene datos sensibles, encripta su contenido con cryptography.Fernet antes de escribir.
  • Almacena el archivo en una carpeta de usuario (ej. ~/.config/miapp/ en Linux, %APPDATA% en Windows) para evitar problemas de permisos.

6. Introducción ligera a SQLite

SQLite es la base de datos embebida más usada en el mundo (incluida en Android, iOS y navegadores). No necesita servidor, solo el archivo .sqlite. A continuación, los conceptos básicos para una lista de tareas.

import sqlite3, pathlib
DB_FILE = pathlib.Path('tasks.sqlite')
def get_conn():
    return sqlite3.connect(DB_FILE)
# 1️⃣ Crear tabla (solo la primera ejecución)
with get_conn() as con:
    con.execute('''
        CREATE TABLE IF NOT EXISTS tasks (
            id TEXT PRIMARY KEY,
            texto TEXT NOT NULL,
            completado INTEGER NOT NULL CHECK (completado IN (0,1))
        );
    ''')
# 2️⃣ Insertar una tarea
import uuid
new_id = str(uuid.uuid4())
with get_conn() as con:
    con.execute('INSERT INTO tasks (id, texto, completado) VALUES (?, ?, ?)',
                (new_id, 'Comprar leche', 0))
# 3️⃣ Leer todas las tareas
with get_conn() as con:
    rows = con.execute('SELECT id, texto, completado FROM tasks').fetchall()
    tasks = [{
        'id': r[0],
        'texto': r[1],
        'completado': bool(r[2])
    } for r in rows]
    print(tasks)
# 4️⃣ Actualizar estado
with get_conn() as con:
    con.execute('UPDATE tasks SET completado = ? WHERE id = ?', (1, new_id))
# 5️⃣ Borrar
with get_conn() as con:
    con.execute('DELETE FROM tasks WHERE id = ?', (new_id,))

Observaciones clave:

  • SQLite usa tipos dinámicos; INTEGER 0/1 es la convención para booleanos.
  • Todo está dentro de with get_conn() as con: lo que garantiza commit automático y cierre seguro.
  • El archivo tasks.sqlite se puede copiar, versionar o abrir con sqlitebrowser para depuración.

7. Caso práctico: lista de tareas usando SQLite

Adaptamos la aplicación anterior para que use la base SQLite en vez de JSON. La lógica de la UI permanece idéntica; solo cambiamos los métodos de carga/guardado.

import sqlite3, pathlib, tkinter as tk, uuid
DB_FILE = pathlib.Path('tasks.sqlite')
class TodoSQLite(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title('🗒️ Tareas (SQLite)')
        self.geometry('400x500')
        self._init_db()
        self.tasks = []
        self._load_tasks()
        self._build_ui()
        self.protocol('WM_DELETE_WINDOW', self._on_close)
    # ---------- DB helpers ----------
    def _get_conn(self):
        return sqlite3.connect(DB_FILE)
    def _init_db(self):
        with self._get_conn() as con:
            con.execute('''
                CREATE TABLE IF NOT EXISTS tasks (
                    id TEXT PRIMARY KEY,
                    texto TEXT NOT NULL,
                    completado INTEGER NOT NULL CHECK (completado IN (0,1))
                );
            ''')
    def _load_tasks(self):
        with self._get_conn() as con:
            rows = con.execute('SELECT id, texto, completado FROM tasks').fetchall()
        self.tasks = [{
            'id': r[0],
            'texto': r[1],
            'completado': bool(r[2])
        } for r in rows]
    def _persist_task(self, task):
        with self._get_conn() as con:
            con.execute('INSERT OR REPLACE INTO tasks (id, texto, completado) VALUES (?, ?, ?)',
                        (task['id'], task['texto'], int(task['completado'])))
    def _delete_from_db(self, task_id):
        with self._get_conn() as con:
            con.execute('DELETE FROM tasks WHERE id = ?', (task_id,))
    # ---------- UI ----------
    def _build_ui(self):
        self.entry = tk.Entry(self, font=('Helvetica', 12))
        self.entry.pack(fill='x', padx=10, pady=5)
        self.entry.bind('', lambda e: self._add_task())
        self.listbox = tk.Listbox(self, font=('Helvetica', 12))
        self.listbox.pack(expand=True, fill='both', padx=10, pady=5)
        self._refresh_listbox()
        btn = tk.Frame(self)
        btn.pack(pady=5)
        tk.Button(btn, text='✔ Completar', command=self._toggle_done).pack(side='left', padx=5)
        tk.Button(btn, text='🗑️ Borrar', command=self._delete_task).pack(side='left', padx=5)
    def _add_task(self):
        txt = self.entry.get().strip()
        if txt:
            task = {'id': str(uuid.uuid4()), 'texto': txt, 'completado': False}
            self.tasks.append(task)
            self._persist_task(task)
            self.entry.delete(0, tk.END)
            self._refresh_listbox()
    def _toggle_done(self):
        sel = self.listbox.curselection()
        if sel:
            idx = sel[0]
            self.tasks[idx]['completado'] = not self.tasks[idx]['completado']
            self._persist_task(self.tasks[idx])
            self._refresh_listbox()
    def _delete_task(self):
        sel = self.listbox.curselection()
        if sel:
            idx = sel[0]
            task_id = self.tasks[idx]['id']
            del self.tasks[idx]
            self._delete_from_db(task_id)
            self._refresh_listbox()
    def _refresh_listbox(self):
        self.listbox.delete(0, tk.END)
        for t in self.tasks:
            pref = '✅' if t['completado'] else '❌'
            self.listbox.insert(tk.END, f"{pref} {t['texto']}")
    def _on_close(self):
        # SQLite ya guardó cada cambio, solo cerramos la ventana
        self.destroy()
if __name__ == '__main__':
    app = TodoSQLite()
    app.mainloop()

Ventajas observadas:

  • Persistencia automática: cada operación escribe en la base, por lo que el cierre no necesita volcar todo de una vez.
  • Escalabilidad: la app sigue siendo fluida con cientos o miles de tareas gracias a los índices implícitos de la PK.
  • Facilidad de migración: si en el futuro deseas sincronizar con un servidor, basta exportar la tabla a CSV o usar sqlite3 en el backend.

8. Buenas prácticas generales de persistencia

Ubicación del archivo

Utiliza rutas específicas del sistema operativo para evitar problemas de permisos y para que la aplicación sea portable:

  • Linux/macOS: ~/.config/miapp/ o ~/Library/Application Support/miapp/.
  • Windows: %APPDATA%\miapp\.

Control de versiones del esquema

Cuando cambies la estructura (añadir campos, renombrar propiedades), incorpora un version tag dentro del archivo JSON o una tabla meta en SQLite. Así podrás migrar datos antiguos de forma automática.

Seguridad y privacidad

  • Encripta datos sensibles con AES‑GCM (ej. cryptography.Fernet).
  • Establece permisos de archivo restrictivos (chmod 600 en Unix).
  • Evita almacenar contraseñas en texto plano; usa gestores de credenciales del SO o token‑based auth.

Gestión de errores y recuperación

Siempre captura IOError, json.JSONDecodeError o sqlite3.DatabaseError. En caso de corrupción, ofrece al usuario la opción de restaurar desde una copia de seguridad automática.

Rendimiento

  • Para JSON, escribe en modo append solo cuando sea necesario (ej. registro de logs), pero realiza flush y fsync antes de cerrar.
  • SQLite: utiliza PRAGMA journal_mode=WAL; para mejorar concurrencia y PRAGMA synchronous=NORMAL; para balancear velocidad y seguridad.

9. Conclusión

Persistir datos en una GUI es una práctica esencial que pasa de ser “un extra bonito” a un requisito de usabilidad. JSON ofrece simplicidad y legibilidad, ideal para configuraciones y listas pequeñas. SQLite, por su parte, brinda robustez, transacciones y capacidad de escalar sin cambiar de arquitectura.

Elige la solución que mejor se adapte al tamaño de tu proyecto, a la complejidad de los datos y a los requerimientos de seguridad. Y, sobre todo, implementa siempre buenas prácticas de manejo de archivos, versionado de esquemas y pruebas de recuperación. Así tu aplicación será fiable, mantenible y preparada para crecer.

 

17 Guardando datos de tu GUI: archivos, JSON y SQLite
ASIMOV Ingeniería S. de R.L. de C.V., Emiliano Nava 10 diciembre, 2025
Compartir
Iniciar sesión dejar un comentario

  
16 Multipantalla en Tkinter: login, menú principal y vistas internas
Guía completa para gestionar múltiples pantallas en Tkinter usando Toplevel o cambiando Frames dentro de una única ventana. Incluye patrón de login → menú principal, código listo para usar y mejores prácticas.