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) - ✔ Sí
- ✔ Sí
- ✔ 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
indentpara que el archivo sea legible y fácil de versionar. - Validar la estructura con jsonschema o
pydanticsi el proyecto es grande. - No almacenar datos sensibles sin cifrado (contraseñas, tokens).
- Siempre envolver la carga en
try/exceptpara 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
uuid4para IDs únicos (evita colisiones si se sincroniza con la nube).
Tips de seguridad:
- Si el archivo contiene datos sensibles, encripta su contenido con
cryptography.Fernetantes 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/1es 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.sqlitese puede copiar, versionar o abrir consqlitebrowserpara 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
sqlite3en 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 600en 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
appendsolo cuando sea necesario (ej. registro de logs), pero realiza flush y fsync antes de cerrar. - SQLite: utiliza
PRAGMA journal_mode=WAL;para mejorar concurrencia yPRAGMA 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