Bases de datos SQLite en Python

📅 Actualizado en marzo 2026 📊 Nivel: Intermedio ⏱️ 18 min de lectura

CSV guarda filas. JSON guarda objetos. Pero ninguno de los dos te permite preguntar: «dame todos los usuarios mayores de 30 que hayan hecho un pedido este mes, ordenados por apellido». Para eso está SQL. Y la forma más directa de tener SQL en Python sin instalar absolutamente nada es SQLite: una base de datos completa que vive en un único fichero y viene incluida en la biblioteca estándar.

Medallón de Cicerón, orador y filósofo romano
Divide et impera — sed quaere prius
«Divide y vencerás — pero pregunta primero»
Adaptación de Cicerón · Orador romano · 106 a.C. – 43 a.C.
La diferencia entre un fichero de datos y una base de datos está en la capacidad de preguntar. Un CSV almacena; una base de datos responde. Antes de escribir la primera línea de código, dedica cinco minutos a pensar qué preguntas vas a hacer a tus datos: eso es exactamente lo que va a determinar el diseño de tus tablas, tus índices y tus consultas.

¿Por qué SQLite?

SQLite es la base de datos más utilizada del mundo. No en servidores empresariales, sino en todo lo demás: está en tu navegador (almacena el historial y las cookies), en tu teléfono (WhatsApp, iOS, Android la usan intensivamente), en aplicaciones de escritorio, en sistemas embebidos. Cuando dices que conoces SQLite, estás hablando de tecnología que corre en literalmente miles de millones de dispositivos.

Para Python, SQLite tiene tres ventajas que lo hacen la opción de partida perfecta:

  • Cero instalación. El módulo sqlite3 viene con Python. No hay servidor que levantar, no hay credenciales que configurar, no hay dependencias externas. Un import sqlite3 y estás dentro.
  • Un solo fichero. Toda la base de datos —esquema, datos, índices— vive en un único fichero .db. Hacer backup es copiar el fichero. Mover la aplicación a otro servidor es mover el fichero.
  • SQL estándar. Lo que aprendes aquí —SELECT, INSERT, WHERE, JOIN— te vale exactamente igual para PostgreSQL o MySQL cuando llegues a necesitarlos. SQLite no es un lenguaje diferente, es el mismo SQL con una implementación simplificada.

Piensa en SQLite como un libro de contabilidad muy bien organizado. Cada tabla es una sección del libro con sus columnas predefinidas. Cada fila es un asiento contable. Y SQL es el lenguaje que te permite buscar cualquier asiento, sumar totales, o reorganizar la información en segundos, sin tener que revisar el libro página a página.

Libro de contabilidad abierto con columnas de números manuscritos en cuadrícula sobre papel amarillento
Un libro de contabilidad: columnas fijas, filas de datos, y la capacidad de sumar, filtrar o buscar cualquier asiento. Las tablas de SQLite funcionan exactamente igual, pero con la potencia de SQL: puedes responder preguntas complejas en milisegundos en lugar de revisar el libro fila a fila. Fuente: Pexels (licencia libre).

La única limitación real de SQLite es la concurrencia de escrituras: solo admite un escritor a la vez. Para una aplicación de escritorio, un script de análisis de datos o un prototipo web, eso no supone ningún problema. Para cientos de usuarios escribiendo simultáneamente, PostgreSQL o MySQL son la opción correcta.

Primera conexión: connect y cursor

El flujo básico de trabajo con sqlite3 tiene siempre la misma estructura: abrir conexión → obtener cursor → ejecutar SQL → confirmar cambios → cerrar.

import sqlite3

# Abrir (o crear si no existe) la base de datos
con = sqlite3.connect('mi_base.db')

# El cursor es el objeto que ejecuta las consultas
cur = con.cursor()

# Ejecutar SQL
cur.execute("SELECT sqlite_version()")
version = cur.fetchone()
print(f"SQLite versión: {version[0]}")

# Cerrar siempre la conexión
con.close()

Si el fichero mi_base.db no existe, connect() lo crea automáticamente. Si existe, lo abre. Hay un caso especial muy útil: sqlite3.connect(':memory:') crea una base de datos en RAM que desaparece al cerrar la conexión. Es perfecta para tests y para procesar datos temporales sin ensuciar el disco.

El patrón recomendado: with

Al igual que con los ficheros de texto, el gestor de contexto with es la forma correcta de trabajar con conexiones SQLite. Garantiza que la conexión se cierra y que los cambios pendientes se confirman (o se descartan si hay una excepción):

import sqlite3

with sqlite3.connect('mi_base.db') as con:
    cur = con.cursor()
    cur.execute("SELECT sqlite_version()")
    print(cur.fetchone()[0])
# La conexión se cierra sola aquí, haya o no excepciones
💡 Rendimiento: Abrir y cerrar una conexión SQLite tiene un coste muy bajo. Si tu script hace muchas operaciones, mantén la conexión abierta durante todo el proceso en lugar de abrir y cerrar en cada operación.

Crear tablas con CREATE TABLE

Antes de insertar datos necesitas definir la estructura de la tabla: qué columnas tiene, qué tipo de dato almacena cada una y qué restricciones aplican.

import sqlite3

with sqlite3.connect('agenda.db') as con:
    cur = con.cursor()
    cur.execute("""
        CREATE TABLE IF NOT EXISTS contactos (
            id      INTEGER PRIMARY KEY,
            nombre  TEXT    NOT NULL,
            email   TEXT    UNIQUE,
            edad    INTEGER,
            activo  INTEGER DEFAULT 1
        )
    """)
    # CREATE TABLE no necesita commit (es DDL, no DML)

Tres detalles que importan:

  • IF NOT EXISTS es obligatorio en scripts que se ejecutan varias veces. Sin él, la segunda ejecución lanzaría un error porque la tabla ya existe.
  • INTEGER PRIMARY KEY autoincrementa. En SQLite, una columna de este tipo se rellena automáticamente con el siguiente número disponible si no especificas un valor al insertar.
  • SQLite tiene tipado flexible. El tipo que declares es una sugerencia, no una obligación estricta como en otros motores. Puedes guardar texto en una columna INTEGER y SQLite no protestará. Esto es conveniente para empezar, pero es mejor ceñirse a los tipos correctos.
Diagrama del ciclo completo de trabajo con SQLite en Python: fichero db, conexión, cursor, ejecución SQL, commit y las cuatro operaciones CRUD con código de ejemplo
Ciclo de vida de una sesión SQLite: desde la conexión al fichero hasta el commit de los cambios. Las cuatro operaciones CRUD y el patrón with recomendado. Infografía: Ciberaula.

Insertar datos: INSERT e executemany

La instrucción INSERT añade filas a una tabla. Lo más importante desde el principio: siempre usa ? como marcador de parámetros, nunca construyas la SQL con f-strings o concatenación de strings. Más sobre esto en la sección de SQL injection.

import sqlite3

with sqlite3.connect('agenda.db') as con:
    cur = con.cursor()

    # Insertar una fila
    cur.execute(
        "INSERT INTO contactos (nombre, email, edad) VALUES (?, ?, ?)",
        ('Ana García', 'ana@ejemplo.com', 32)
    )
    con.commit()

    # Recuperar el ID asignado automáticamente
    print(f"ID asignado: {cur.lastrowid}")

Insertar múltiples filas con executemany

Si tienes una lista de registros para insertar, executemany() es mucho más eficiente que un bucle de execute(). Internamente lo agrupa en una sola transacción:

contactos = [
    ('Luis Martín',  'luis@ejemplo.com',  28),
    ('Sara López',   'sara@ejemplo.com',  41),
    ('Carlos Pérez', 'carlos@ejemplo.com', 35),
]

with sqlite3.connect('agenda.db') as con:
    cur = con.cursor()
    cur.executemany(
        "INSERT INTO contactos (nombre, email, edad) VALUES (?, ?, ?)",
        contactos
    )
    con.commit()
    print(f"Filas insertadas: {cur.rowcount}")

Para insertar datos de un CSV directamente en SQLite (patrón muy habitual en data pipelines):

import csv
import sqlite3

with sqlite3.connect('agenda.db') as con, \
     open('contactos.csv', 'r', encoding='utf-8', newline='') as f:
    cur = con.cursor()
    reader = csv.DictReader(f)
    datos = [(r['nombre'], r['email'], int(r['edad'])) for r in reader]
    cur.executemany(
        "INSERT INTO contactos (nombre, email, edad) VALUES (?, ?, ?)",
        datos
    )
    con.commit()

Consultar datos: SELECT y filtros

El SELECT es la operación más importante de SQL y la que más vas a escribir. A diferencia de INSERT, UPDATE y DELETE, el SELECT no modifica datos y no necesita commit.

with sqlite3.connect('agenda.db') as con:
    cur = con.cursor()

    # Todos los contactos
    cur.execute("SELECT * FROM contactos")
    todos = cur.fetchall()          # lista de tuples

    # Con filtro WHERE y parámetros seguros
    cur.execute(
        "SELECT nombre, email FROM contactos WHERE edad > ? ORDER BY nombre",
        (30,)
    )
    mayores = cur.fetchall()

    # Procesar fila a fila (eficiente con grandes conjuntos)
    cur.execute("SELECT * FROM contactos")
    for fila in cur:                # el cursor es iterable
        print(fila[1], fila[2])     # fila es un tuple: fila[0]=id, fila[1]=nombre...

Los tres métodos de lectura que tienes disponibles:

MétodoDevuelveCuándo usarlo
fetchone()Un tuple o NoneCuando esperas un único resultado
fetchall()Lista de tuplesCuando el conjunto cabe en RAM
fetchmany(n)Lista de n tuplesPara conjuntos grandes: procesa en lotes

Funciones de agregación

with sqlite3.connect('agenda.db') as con:
    cur = con.cursor()

    cur.execute("SELECT COUNT(*) FROM contactos")
    total = cur.fetchone()[0]
    print(f"Total de contactos: {total}")

    cur.execute("SELECT AVG(edad), MAX(edad), MIN(edad) FROM contactos")
    avg, maximo, minimo = cur.fetchone()
    print(f"Edad media: {avg:.1f}, máxima: {maximo}, mínima: {minimo}")

    # Agrupar por valor
    cur.execute("SELECT activo, COUNT(*) FROM contactos GROUP BY activo")
    for activo, cantidad in cur.fetchall():
        estado = 'Activos' if activo else 'Inactivos'
        print(f"{estado}: {cantidad}")
Libreta abierta junto a un teclado y una taza de café sobre escritorio de madera clara
Una libreta portátil que no necesita enchufarse a ningún servidor para funcionar: la abres, escribes o consultas, y la guardas. SQLite es exactamente eso: toda la base de datos en un fichero portable que puedes abrir, consultar y cerrar sin ninguna infraestructura adicional. Fuente: Pexels (licencia libre).

UPDATE y DELETE

Ambas operaciones modifican datos existentes y, por tanto, requieren commit() después. Y ambas deben ir siempre acompañadas de un WHERE para no afectar a todas las filas de la tabla accidentalmente.

with sqlite3.connect('agenda.db') as con:
    cur = con.cursor()

    # Actualizar la edad de Ana
    cur.execute(
        "UPDATE contactos SET edad = ? WHERE nombre = ?",
        (33, 'Ana García')
    )
    con.commit()
    print(f"Filas actualizadas: {cur.rowcount}")

    # Desactivar contactos mayores de 60
    cur.execute(
        "UPDATE contactos SET activo = 0 WHERE edad > ?",
        (60,)
    )
    con.commit()

    # Eliminar un contacto por ID
    cur.execute(
        "DELETE FROM contactos WHERE id = ?",
        (1,)
    )
    con.commit()
    print(f"Filas eliminadas: {cur.rowcount}")

cur.rowcount siempre indica cuántas filas ha afectado la última operación. Es muy útil para verificar que el UPDATE o DELETE ha tocado exactamente las filas esperadas (y no cero, que indicaría que el WHERE no ha encontrado nada).

⚠️ El UPDATE y DELETE sin WHERE afectan a todas las filas. "UPDATE contactos SET activo = 0" sin ningún WHERE desactiva absolutamente todos los contactos. Antes de ejecutar un UPDATE o DELETE en producción, prueba primero el mismo WHERE con un SELECT para verificar exactamente qué filas va a tocar.

row_factory: filas como diccionarios

Por defecto, sqlite3 devuelve cada fila como un tuple, lo que obliga a recordar el índice de cada columna: fila[0] es el id, fila[1] es el nombre... Esto es frágil: si alguien añade una columna en medio de la tabla, todos los índices se desplazan.

La solución es row_factory. Configurando con.row_factory = sqlite3.Row, cada fila devuelta pasa a ser un objeto que admite acceso tanto por índice como por nombre de columna:

import sqlite3

with sqlite3.connect('agenda.db') as con:
    con.row_factory = sqlite3.Row   # ← configurar ANTES de crear el cursor

    cur = con.cursor()
    cur.execute("SELECT * FROM contactos WHERE activo = 1")

    for fila in cur.fetchall():
        # Acceso por nombre — claro y resistente a cambios de esquema
        print(f"{fila['nombre']} ({fila['edad']}) — {fila['email']}")

        # También funciona por índice
        print(fila[0])   # → id

        # Y se puede convertir a dict
        d = dict(fila)
        print(d)         # → {'id': 1, 'nombre': 'Ana García', ...}

Si necesitas un dict directamente (por ejemplo, para serializar a JSON), puedes usar una factory personalizada:

def dict_factory(cursor, row):
    return {col[0]: row[i] for i, col in enumerate(cursor.description)}

con.row_factory = dict_factory
# Ahora fetchone() y fetchall() devuelven dicts directamente
Ficha de referencia de sqlite3: tipos de datos, métodos fetchone fetchall fetchmany, row_factory, executemany y ejemplos de SQL con SELECT ORDER BY y funciones de agregación
Referencia completa del módulo sqlite3: tipos, métodos de lectura, acceso por nombre con row_factory, inserción masiva con executemany y patrones SQL esenciales. Ficha de referencia: Ciberaula.

SQL injection y parámetros seguros

SQL injection es el ataque más clásico en el mundo de las bases de datos. Ocurre cuando introduces texto del usuario directamente en una consulta SQL, permitiendo que alguien construya una consulta maliciosa que modifique o exponga datos que no debería.

En Python, la forma incorrecta (y peligrosa) de insertar valores en SQL es con f-strings o concatenación:

# ❌ NUNCA hacer esto — SQL injection posible
nombre = input("¿Cuál es tu nombre? ")
cur.execute(f"SELECT * FROM contactos WHERE nombre = '{nombre}'")

# Si el usuario escribe: ' OR '1'='1
# La consulta resultante sería:
# SELECT * FROM contactos WHERE nombre = '' OR '1'='1'
# → devuelve TODOS los contactos, sin importar el nombre

La forma correcta es siempre usar marcadores de parámetros ?. El módulo sqlite3 se encarga de escapar correctamente los valores:

# ✅ Siempre así — parámetros seguros
nombre = input("¿Cuál es tu nombre? ")
cur.execute("SELECT * FROM contactos WHERE nombre = ?", (nombre,))

# El módulo sqlite3 trata el nombre como un valor de dato,
# nunca como parte del código SQL

Nota el tuple en el segundo argumento: aunque solo tengas un parámetro, debes pasarlo dentro de un tuple. (nombre,) es un tuple de un elemento; (nombre) sin la coma final es simplemente nombre entre paréntesis.

⚠️ Esta regla no tiene excepciones. No importa que el valor venga de tu propio código, de un fichero de configuración o de una base de datos de confianza. Los parámetros ? son siempre la forma correcta. El tiempo que tardan en escribirse es cero; el tiempo que cuesta limpiar una base de datos corrompida por SQL injection puede ser semanas.

Un programa completo de ejemplo

import sqlite3

def crear_agenda():
    with sqlite3.connect('agenda.db') as con:
        con.row_factory = sqlite3.Row
        cur = con.cursor()

        # Crear tabla
        cur.execute("""
            CREATE TABLE IF NOT EXISTS contactos (
                id     INTEGER PRIMARY KEY,
                nombre TEXT    NOT NULL,
                email  TEXT    UNIQUE,
                edad   INTEGER
            )
        """)

        # Insertar datos de ejemplo
        muestra = [
            ('Ana García',   'ana@ej.com',    32),
            ('Luis Martín',  'luis@ej.com',   28),
            ('Sara López',   'sara@ej.com',   41),
        ]
        cur.executemany(
            "INSERT OR IGNORE INTO contactos (nombre, email, edad) VALUES (?, ?, ?)",
            muestra
        )
        con.commit()

        # Consultar y mostrar
        cur.execute("SELECT * FROM contactos ORDER BY nombre")
        for c in cur.fetchall():
            print(f"[{c['id']}] {c['nombre']} — {c['email']} — {c['edad']} años")

crear_agenda()

INSERT OR IGNORE es una variante especialmente útil: si la fila ya existe (porque viola una restricción UNIQUE, como el email), simplemente la ignora en lugar de lanzar un error. Perfecto para scripts que se ejecutan varias veces.

❓ Preguntas frecuentes

❓ Preguntas frecuentes sobre Bases de datos SQLite en Python

Las dudas más comunes respondidas de forma clara y directa.

Usa SQLite cuando necesitas consultar o filtrar los datos de forma flexible (WHERE, ORDER BY, JOIN), cuando los datos se modifican frecuentemente (insertar, actualizar, eliminar), cuando necesitas garantizar integridad (restricciones NOT NULL, UNIQUE, claves foráneas), o cuando el volumen de datos crece y necesitas acceso eficiente. CSV y JSON son mejores para intercambio de datos entre sistemas o cuando la estructura es muy simple y prácticamente de solo lectura.
Solo después de operaciones de escritura: INSERT, UPDATE y DELETE. Los SELECT no modifican datos, así que no necesitan commit. Por defecto, sqlite3 trabaja en modo transaccional: los cambios se acumulan hasta que haces commit(). Si cierras la conexión sin hacer commit(), los cambios se descartan automáticamente. Si usas el patrón with sqlite3.connect(...) as con:, el commit se hace automáticamente al salir del bloque (si no hay excepciones), lo que simplifica el código.
Puedes consultar el esquema de cualquier tabla con: cur.execute("SELECT name, type FROM pragma_table_info('nombre_tabla')"). Esto devuelve el nombre y tipo de cada columna. Otra opción es ejecutar cur.execute("SELECT * FROM nombre_tabla LIMIT 1") y luego consultar cur.description, que contiene los metadatos de las columnas de la última consulta ejecutada.
Depende del volumen. SQLite tiene un sistema de bloqueo a nivel de fichero: admite múltiples lecturas simultáneas, pero solo una escritura a la vez. Para aplicaciones web con pocos usuarios (hasta decenas) y escrituras poco frecuentes, funciona perfectamente bien. De hecho, lo usan en producción sitios con millones de visitas mensuales. Si tienes muchas escrituras concurrentes o cientos de usuarios activos escribiendo a la vez, PostgreSQL o MySQL son la opción correcta.
La forma más simple es copiar el fichero .db: al ser un solo fichero portable, una copia de seguridad es literalmente una copia del fichero. Para backups mientras la base de datos está en uso, el módulo sqlite3 ofrece el método con.backup(destino), que crea una copia consistente aunque haya operaciones en curso. También puedes usar el comando sqlite3 mi_base.db .dump > backup.sql desde la terminal para exportar a SQL legible.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Bases de datos SQLite en Python? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!