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.
¿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=4para que el archivo sea legible. - Valida la estructura con
jsonschemasi 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=WALpara 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
cryptographypara encriptar valores antes de guardarlos en JSON o SQLite. - Permisos de archivo: en Linux,
chmod 600 file.dbychmod 600 config.jsonpara 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
WHEREoORDER 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. conrsyncocron). - Para JSON, guarda versiones con timestamp:
config_20251210.json.
Depuración y troubleshooting
- JSON corrupto: usa
json.tooldesde 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.