¿Qué vamos a construir?
Una aplicación de escritorio para gestionar usuarios, construida completamente en Python con Flet como capa de interfaz y SQLite como base de datos local. Sin servidor web, sin dependencias externas, sin conexión a internet.
La app incluye:
- Crear, listar, editar y eliminar usuarios (CRUD completo)
- Búsqueda en tiempo real por nombre o email mientras el usuario escribe
- Diálogo de confirmación antes de eliminar un registro
- Mensajes flash que desaparecen solos después de 3 segundos
- Botón de backup que genera una copia de la base de datos con timestamp
- Contador de usuarios registrados visible en todo momento
data/usuarios.db dentro del proyecto. No se envía nada a ningún servidor. Es ideal para herramientas internas, utilidades de escritorio o apps educativas.
Instalar dependencias
Este proyecto tiene una sola dependencia externa: Flet. SQLite viene incluido en Python desde la biblioteca estándar, así que no necesitas instalar nada más para la base de datos.
Instala Flet directamente con pip:
pip install flet
O si ya clonaste el repositorio, usa el archivo requirements.txt:
pip install -r requirements.txt
El contenido de requirements.txt es mínimo:
flet>=0.28.3
python -m venv venv y luego venv\Scripts\activate en Windows o source venv/bin/activate en Linux/macOS.
Estructura del proyecto
El proyecto sigue una separación clara entre la lógica de datos y la interfaz de usuario, encapsulando cada responsabilidad en su propio módulo:
FletSQLite/
├── main.py # Punto de entrada de la aplicación
├── requirements.txt # Dependencias (solo flet)
└── src/
├── __init__.py # Convierte src en paquete Python
├── database.py # Clase DatabaseManager — toda la lógica SQLite
└── ui.py # Clase UserInterface — toda la interfaz Flet
La carpeta data/ con el archivo usuarios.db se crea automáticamente la primera vez que corres la app. No necesitas crearla manualmente.
La capa de datos: database.py
La clase DatabaseManager centraliza toda la interacción con SQLite. En el constructor se asegura de que la carpeta data/ exista y luego inicializa la base de datos:
import sqlite3
import os
import shutil
from datetime import datetime
from typing import Optional
class DatabaseManager:
def __init__(self):
os.makedirs("data", exist_ok=True)
self.db_path = "data/usuarios.db"
self.init_database()
El método init_database() crea la tabla usuarios si no existe. El campo email tiene restricción UNIQUE para evitar duplicados, y fecha_registro se llena automáticamente con la fecha actual:
def init_database(self):
with sqlite3.connect(self.db_path) as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS usuarios (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
fecha_registro TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_usuarios_email
ON usuarios(email)
""")
conn.commit()
El método agregar_usuario() captura la excepción IntegrityError que SQLite lanza cuando se intenta registrar un email duplicado, y retorna un mensaje de error claro en español:
def agregar_usuario(self, nombre: str, email: str) -> tuple[bool, str]:
try:
with sqlite3.connect(self.db_path) as conn:
conn.execute(
"INSERT INTO usuarios (nombre, email) VALUES (?, ?)",
(nombre.strip(), email.strip().lower())
)
conn.commit()
return True, "Usuario agregado correctamente"
except sqlite3.IntegrityError:
return False, "El email ya está registrado"
El resto de métodos de la clase siguen el mismo patrón con conexiones de contexto (with sqlite3.connect(...)):
obtener_usuarios()— retorna todos los registros ordenados por nombreactualizar_usuario()— recibe id, nombre y email, también capturaIntegrityErroreliminar_usuario()— elimina por idbuscar_usuarios()— búsqueda conLIKE '%termino%'en nombre y emailcontar_usuarios()— retorna el total de registros como enterobackup_database()— copia el archivo.dbconshutil.copy2y un timestamp en el nombre
La interfaz: ui.py
La clase UserInterface recibe la página de Flet y el DatabaseManager en su constructor. La inicialización ocurre en dos métodos separados: configuración de la página y creación de componentes.
Configuración de la página y los campos de entrada:
import flet as ft
import threading
from datetime import datetime
class UserInterface:
def __init__(self, page: ft.Page, db_manager):
self.page = page
self.db = db_manager
self.usuario_editando_id = None
self._setup_page()
self._init_components()
def _setup_page(self):
self.page.title = "Gestión de Usuarios"
self.page.theme_mode = ft.ThemeMode.LIGHT
self.page.padding = 20
self.page.window_width = 800
self.page.window_height = 600
def _init_components(self):
self.campo_nombre = ft.TextField(
label="Nombre",
hint_text="Nombre completo",
expand=True
)
self.campo_email = ft.TextField(
label="Email",
hint_text="correo@ejemplo.com",
expand=True
)
self.campo_busqueda = ft.TextField(
label="Buscar usuario",
hint_text="Nombre o email...",
on_change=self._buscar_en_tiempo_real,
expand=True
)
self.btn_accion = ft.ElevatedButton(
text="Agregar usuario",
on_click=self._manejar_accion,
icon=ft.icons.PERSON_ADD
)
self.lbl_mensaje = ft.Text(value="", color=ft.colors.GREEN)
self.lbl_contador = ft.Text(value="")
self.tabla_usuarios = ft.DataTable(
columns=[
ft.DataColumn(ft.Text("ID")),
ft.DataColumn(ft.Text("Nombre")),
ft.DataColumn(ft.Text("Email")),
ft.DataColumn(ft.Text("Fecha registro")),
ft.DataColumn(ft.Text("Acciones")),
],
rows=[]
)
Botón toggle para crear y editar: el mismo botón btn_accion funciona tanto para agregar como para actualizar. El estado se controla con self.usuario_editando_id: si es None, se crea; si tiene un valor, se actualiza. Al terminar la edición se limpia el estado y el texto del botón vuelve a "Agregar usuario":
def _manejar_accion(self, e):
nombre = self.campo_nombre.value.strip()
email = self.campo_email.value.strip()
if not nombre or not email:
self._mostrar_mensaje("Nombre y email son obligatorios", error=True)
return
if self.usuario_editando_id is None:
ok, msg = self.db.agregar_usuario(nombre, email)
else:
ok, msg = self.db.actualizar_usuario(
self.usuario_editando_id, nombre, email
)
if ok:
self.usuario_editando_id = None
self.btn_accion.text = "Agregar usuario"
self.btn_accion.icon = ft.icons.PERSON_ADD
self._mostrar_mensaje(msg, error=not ok)
if ok:
self._limpiar_campos()
self._actualizar_tabla()
Mensajes flash con threading.Timer: el mensaje aparece, la página se actualiza, y un timer lo borra automáticamente a los 3 segundos sin bloquear la interfaz:
def _mostrar_mensaje(self, texto: str, error: bool = False):
self.lbl_mensaje.value = texto
self.lbl_mensaje.color = ft.colors.RED if error else ft.colors.GREEN
self.page.update()
def limpiar():
self.lbl_mensaje.value = ""
self.page.update()
threading.Timer(3.0, limpiar).start()
Confirmación antes de eliminar con ft.AlertDialog: antes de borrar cualquier registro se muestra un diálogo modal. Solo si el usuario confirma se llama a db.eliminar_usuario():
def _eliminar_usuario_confirmacion(self, usuario_id: int, nombre: str):
def confirmar(e):
dialogo.open = False
self.page.update()
ok, msg = self.db.eliminar_usuario(usuario_id)
self._mostrar_mensaje(msg, error=not ok)
if ok:
self._actualizar_tabla()
def cancelar(e):
dialogo.open = False
self.page.update()
dialogo = ft.AlertDialog(
modal=True,
title=ft.Text("Confirmar eliminación"),
content=ft.Text(f"¿Eliminar a {nombre}? Esta acción no se puede deshacer."),
actions=[
ft.TextButton("Cancelar", on_click=cancelar),
ft.ElevatedButton(
"Eliminar",
on_click=confirmar,
bgcolor=ft.colors.RED,
color=ft.colors.WHITE
),
]
)
self.page.dialog = dialogo
dialogo.open = True
self.page.update()
El método build() ensambla todos los componentes en la jerarquía de controles de Flet y lo retorna para que main.py lo agregue a la página:
def build(self) -> ft.Column:
self._actualizar_tabla()
btn_backup = ft.OutlinedButton(
text="Backup",
icon=ft.icons.SAVE,
on_click=self._hacer_backup
)
return ft.Column(
controls=[
ft.Text("Gestión de Usuarios", size=24, weight=ft.FontWeight.BOLD),
ft.Row([self.campo_nombre, self.campo_email]),
ft.Row([self.btn_accion, btn_backup]),
self.lbl_mensaje,
ft.Divider(),
ft.Row([self.campo_busqueda, self.lbl_contador]),
self.tabla_usuarios,
],
scroll=ft.ScrollMode.AUTO,
expand=True
)
El punto de entrada: main.py
El archivo main.py es el punto de entrada de la aplicación. Instancia el DatabaseManager, crea la UserInterface y agrega el componente raíz a la página de Flet:
import flet as ft
from src.database import DatabaseManager
from src.ui import UserInterface
def main(page: ft.Page):
db_manager = DatabaseManager()
ui = UserInterface(page, db_manager)
page.add(ui.build())
if __name__ == "__main__":
ft.app(target=main, view=ft.WEB_BROWSER)
El parámetro view=ft.WEB_BROWSER hace que Flet abra la aplicación en el navegador por defecto en lugar de una ventana de escritorio nativa. Si prefieres la ventana de escritorio, puedes cambiar a view=ft.FLET_APP o simplemente omitir el parámetro view.
localhost solo para renderizar la interfaz. No hay nada en internet.
Correr la app
Con las dependencias instaladas, corre la app desde la raíz del proyecto:
python main.py
Flet abrirá automáticamente tu navegador con la interfaz. La primera vez que corras la app, se crea la carpeta data/ y el archivo usuarios.db dentro.
El flujo completo que deberías poder hacer sin errores:
- Escribir un nombre y un email en los campos y pulsar "Agregar usuario" — aparece en la tabla y el contador sube
- Escribir en el campo de búsqueda y ver cómo la tabla filtra en tiempo real
- Pulsar el botón de editar en cualquier fila — los datos se cargan en los campos y el botón cambia a "Actualizar usuario"
- Modificar el nombre o el email y confirmar la edición
- Pulsar el botón de eliminar — aparece el diálogo de confirmación antes de borrar
- Intentar agregar un email duplicado — la app muestra el mensaje de error sin romper nada
- Pulsar "Backup" — se genera un archivo
data/usuarios_backup_YYYYMMDD_HHMMSS.db