Ir al contenido

IDEAS PRÁCTICAS · ASIMOV INGENIERÍA

Guardando datos de tu GUI: archivos, JSON y SQLite

Aprende por qué y cómo persistir la configuración, listas y registros de tu aplicación de escritorio usando archivos de texto, JSON y SQLite, con ejemplos prácticos y buenas prácticas.

Imagen ilustrativa · Growtika / Unsplash.

Table of contents

No titles in the blog post

Guardando datos de tu GUI: archivos, JSON y SQLite

Una guía completa para almacenar configuración, listas y registros en aplicaciones de escritorio, con ejemplos en Python y buenas prácticas de seguridad y rendimiento.

Cables de fibra óptica conectados a equipos
Imagen ilustrativa · Albert Stoynov / Unsplash.

¿Por qué es útil guardar datos?

Una aplicación que pierde su estado cada vez que el usuario la cierra genera una mala experiencia. Guardar datos permite:

  • Persistencia de la configuración: colores, temas, rutas de archivos, preferencias de usuario.
  • Historial de uso: registros de acciones, auditorías, estadísticas.
  • Listas y colecciones: listas de tareas, inventarios, contactos, etc.
  • Recuperación ante fallos: si la app se cierra inesperadamente, los datos pueden restaurarse.

Opciones de persistencia en aplicaciones de escritorio

Existen varias alternativas, cada una con ventajas y limitaciones. A continuación una tabla comparativa (máximo dos columnas) que ayuda a decidir cuál usar.

Archivo plano (texto, CSV)
  • Simplicidad absoluta.
  • Sin dependencias externas.
  • Ideal para configuraciones muy pequeñas.
JSON
  • Estructura jerárquica (objetos y arrays).
  • Legible por humanos y fácil de depurar.
  • Amplio soporte en todos los lenguajes modernos.
SQLite
  • Base de datos relacional ligera (un solo archivo).
  • Consultas SQL, índices, transacciones ACID.
  • Escala mucho mejor que JSON para miles de filas.
Otros (XML, YAML, Realm, TinyDB)
  • Casos de uso específicos.
  • Requieren bibliotecas adicionales.
  • Mayor complejidad de mantenimiento.

Trabajando con JSON

Python incluye el módulo json en la biblioteca estándar. A continuación ejemplos de escritura, lectura y manejo de errores.

import json, os

DATA_FILE = 'config.json'

# ------------------- Guardar datos -------------------
def save_config(config: dict, path: str = DATA_FILE) -> None:
    try:
        with open(path, 'w', encoding='utf-8') as f:
            json.dump(config, f, ensure_ascii=False, indent=4)
        print(f"Configuración guardada en {path}")
    except OSError as e:
        print(f"Error al escribir el archivo: {e}")

# ------------------- Cargar datos -------------------
def load_config(path: str = DATA_FILE) -> dict:
    if not os.path.exists(path):
        return {}
    try:
        with open(path, 'r', encoding='utf-8') as f:
            return json.load(f)
    except (OSError, json.JSONDecodeError) as e:
        print(f"Error al leer el JSON: {e}")
        return {}

# ------------------- Uso -------------------
if __name__ == '__main__':
    cfg = load_config()
    cfg.setdefault('theme', 'light')
    cfg['last_open'] = '2025-12-10'
    save_config(cfg)

Consejos de buenas prácticas:

  • Usa indent=4 para que el archivo sea legible.
  • Valida la estructura con jsonschema si el formato es crítico.
  • Considera encriptar datos sensibles (p. ej. contraseñas) antes de guardarlos.

Introducción a SQLite

SQLite es una base de datos embebida, sin servidor, que almacena toda la información en un único archivo (.db). Es perfecta para aplicaciones de escritorio que necesitan:

  • Consultas rápidas y filtrado avanzado.
  • Relaciones entre tablas (p. ej. tareas y etiquetas).
  • Transacciones seguras (ACID).

Ejemplo básico de creación, inserción y consulta usando sqlite3 (también incluido en la stdlib).

import sqlite3

DB_PATH = 'tasks.db'

def init_db():
    conn = sqlite3.connect(DB_PATH)
    cur = conn.cursor()
    cur.execute('''
        CREATE TABLE IF NOT EXISTS tasks (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            completed INTEGER NOT NULL DEFAULT 0,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    ''')
    conn.commit()
    conn.close()

def add_task(title: str):
    conn = sqlite3.connect(DB_PATH)
    cur = conn.cursor()
    cur.execute('INSERT INTO tasks (title) VALUES (?)', (title,))
    conn.commit()
    conn.close()

def get_tasks():
    conn = sqlite3.connect(DB_PATH)
    cur = conn.cursor()
    cur.execute('SELECT id, title, completed FROM tasks ORDER BY created_at DESC')
    rows = cur.fetchall()
    conn.close()
    return rows

Aspectos de rendimiento y seguridad:

  • Usa índices (CREATE INDEX) en columnas de búsqueda frecuente.
  • Activa PRAGMA journal_mode = WAL; para mejor concurrencia.
  • Protege el archivo con permisos de sistema (chmod 600) y, si es necesario, cifra con SQLCipher.

Ejemplo práctico: App de lista de tareas

Una pequeña aplicación Tkinter que guarda sus tareas en SQLite al cerrar y las recarga al iniciar.

import tkinter as tk
from tkinter import ttk, messagebox
import sqlite3
import os

DB_FILE = 'todo.db'

class TodoApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title('Lista de tareas')
        self.geometry('400x500')
        self._setup_ui()
        self._init_db()
        self._load_tasks()
        self.protocol('WM_DELETE_WINDOW', self.on_close)

    # ---------- UI ----------
    def _setup_ui(self):
        self.entry = ttk.Entry(self)
        self.entry.pack(fill='x', padx=10, pady=5)
        self.entry.bind('', lambda e: self.add_task())
        self.listbox = tk.Listbox(self, selectmode='multiple')
        self.listbox.pack(fill='both', expand=True, padx=10, pady=5)
        btn_frame = ttk.Frame(self)
        btn_frame.pack(fill='x', padx=10, pady=5)
        ttk.Button(btn_frame, text='Añadir', command=self.add_task).pack(side='left')
        ttk.Button(btn_frame, text='Eliminar', command=self.delete_selected).pack(side='right')

    # ---------- DB ----------
    def _init_db(self):
        self.conn = sqlite3.connect(DB_FILE)
        self.conn.execute('PRAGMA journal_mode=WAL;')
        self.conn.execute('''
            CREATE TABLE IF NOT EXISTS tasks (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                completed INTEGER NOT NULL DEFAULT 0
            )
        ''')
        self.conn.commit()

    def _load_tasks(self):
        cur = self.conn.cursor()
        cur.execute('SELECT id, title, completed FROM tasks ORDER BY id DESC')
        for tid, title, completed in cur.fetchall():
            prefix = '[x] ' if completed else '[ ] '
            self.listbox.insert('end', f"{tid}:{prefix}{title}")

    # ---------- Operaciones ----------
    def add_task(self):
        title = self.entry.get().strip()
        if not title:
            messagebox.showwarning('Entrada vacía', 'Escribe una tarea antes de añadir.')
            return
        cur = self.conn.cursor()
        cur.execute('INSERT INTO tasks (title) VALUES (?)', (title,))
        self.conn.commit()
        tid = cur.lastrowid
        self.listbox.insert('end', f"{tid}:[ ] {title}")
        self.entry.delete(0, 'end')

    def delete_selected(self):
        selected = self.listbox.curselection()
        if not selected:
            return
        cur = self.conn.cursor()
        for idx in reversed(selected):
            item = self.listbox.get(idx)
            tid = int(item.split(':')[0])
            cur.execute('DELETE FROM tasks WHERE id=?', (tid,))
            self.listbox.delete(idx)
        self.conn.commit()

    def on_close(self):
        # Cerrar la conexión de forma segura
        self.conn.close()
        self.destroy()

if __name__ == '__main__':
    app = TodoApp()
    app.mainloop()

Aspectos a destacar:

  • Uso de PRAGMA journal_mode=WAL para que la UI siga respondiendo mientras se escribe.
  • Persistencia automática al cerrar mediante protocol('WM_DELETE_WINDOW').
  • Identificador único (id) almacenado en la lista para operaciones de borrado.

Mejores prácticas y consideraciones avanzadas

Seguridad

  • Cifrado de datos sensibles: usa cryptography para encriptar valores antes de guardarlos en JSON o SQLite.
  • Permisos de archivo: en Linux, chmod 600 file.db y chmod 600 config.json para que sólo el usuario propietario pueda leer/escribir.
  • Validación de entrada: nunca confíes en datos introducidos por el usuario; sanitiza antes de insertarlos en la base.

Rendimiento y escalabilidad

  • Para JSON, evita archivos gigantes (>10 MB). Si la lista supera ese tamaño, migra a SQLite.
  • En SQLite, crea índices en columnas usadas en WHERE o ORDER BY.
  • Utiliza executemany() para inserciones masivas y reduce la sobrecarga de I/O.

Backup y recuperación

  • Copias de seguridad periódicas del archivo .db (p. ej. con rsync o cron).
  • Para JSON, guarda versiones con timestamp: config_20251210.json.

Depuración y troubleshooting

  • JSON corrupto: usa json.tool desde la línea de comandos para validar.
  • SQLite bloqueado: verifica que no haya procesos colgando con lsof | grep .db.
  • Errores de concurrencia: habilita PRAGMA busy_timeout = 5000; para esperar 5 s antes de lanzar excepción.

© 2025 TuNombre – Guías de desarrollo de software. Todos los derechos reservados.