Bases de datos SQLite en Python
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.
¿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
sqlite3viene con Python. No hay servidor que levantar, no hay credenciales que configurar, no hay dependencias externas. Unimport sqlite3y 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.
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
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 EXISTSes 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 KEYautoincrementa. 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.
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étodo | Devuelve | Cuándo usarlo |
|---|---|---|
fetchone() | Un tuple o None | Cuando esperas un único resultado |
fetchall() | Lista de tuples | Cuando el conjunto cabe en RAM |
fetchmany(n) | Lista de n tuples | Para 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}")
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).
"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
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.
? 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.
💬 Foro de discusión
¿Tienes dudas sobre Bases de datos SQLite en Python? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!