Blog Python

CRUD con Flet y SQLite: gestión de usuarios sin servidor

Flet te permite construir interfaces gráficas en Python puro, sin HTML ni JavaScript. En este tutorial construimos una app de escritorio completa: CRUD de usuarios sobre SQLite, búsqueda en tiempo real, confirmación antes de eliminar y backup con un clic. Todo corre localmente, sin servidor ni internet.

01

¿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:

  1. Crear, listar, editar y eliminar usuarios (CRUD completo)
  2. Búsqueda en tiempo real por nombre o email mientras el usuario escribe
  3. Diálogo de confirmación antes de eliminar un registro
  4. Mensajes flash que desaparecen solos después de 3 segundos
  5. Botón de backup que genera una copia de la base de datos con timestamp
  6. Contador de usuarios registrados visible en todo momento
Los datos se guardan en 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.
02

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
Se recomienda usar un entorno virtual para no contaminar tu instalación global de Python: python -m venv venv y luego venv\Scripts\activate en Windows o source venv/bin/activate en Linux/macOS.
03

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.

04

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 nombre
  • actualizar_usuario() — recibe id, nombre y email, también captura IntegrityError
  • eliminar_usuario() — elimina por id
  • buscar_usuarios() — búsqueda con LIKE '%termino%' en nombre y email
  • contar_usuarios() — retorna el total de registros como entero
  • backup_database() — copia el archivo .db con shutil.copy2 y un timestamp en el nombre
05

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
        )
06

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.

Aunque la app se abre en el navegador, sigue siendo una aplicación local. Flet levanta un servidor HTTP interno en localhost solo para renderizar la interfaz. No hay nada en internet.
07

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:

  1. Escribir un nombre y un email en los campos y pulsar "Agregar usuario" — aparece en la tabla y el contador sube
  2. Escribir en el campo de búsqueda y ver cómo la tabla filtra en tiempo real
  3. Pulsar el botón de editar en cualquier fila — los datos se cargan en los campos y el botón cambia a "Actualizar usuario"
  4. Modificar el nombre o el email y confirmar la edición
  5. Pulsar el botón de eliminar — aparece el diálogo de confirmación antes de borrar
  6. Intentar agregar un email duplicado — la app muestra el mensaje de error sin romper nada
  7. Pulsar "Backup" — se genera un archivo data/usuarios_backup_YYYYMMDD_HHMMSS.db
jamesmosq / FletSQLite
Código completo de este tutorial — CRUD de usuarios con Flet y SQLite, búsqueda en tiempo real, confirmación de eliminación y backup.
Ver en GitHub
Volver al blog