Base de datos SQLite en Python, sin instalar nada

SQLite viene dentro de Python. No hay que instalar un controlador, ni un servidor, ni configurar nada: es un archivo y un módulo de la biblioteca estándar. Para una herramienta, una caché o las pruebas, es difícil de superar.

Conectar

import sqlite3
from pathlib import Path

with sqlite3.connect(Path("biblioteca.db")) as cn:
    cn.execute("PRAGMA foreign_keys = ON")      # no está activo por omisión
    cn.row_factory = sqlite3.Row                # filas como diccionarios
salidamotor SQLite 3.46.1   (viene con Python: cero dependencias)

Dos líneas que conviene poner siempre. Las claves ajenas en SQLite están desactivadas por omisión por compatibilidad histórica, así que tu REFERENCES no hace nada hasta que lo enciendes. Y row_factory cambia las tuplas por filas con nombre: f["titulo"] en vez de f[1].

Insertar

cur = cn.execute("INSERT INTO libros (titulo, autor, anio) VALUES (?, ?, ?)",
                 ("Rayuela", "Julio Cortázar", 1963))
cur.lastrowid

cn.executemany("INSERT INTO libros (titulo, autor, anio) VALUES (?, ?, ?)",
               [("Pedro Páramo","Juan Rulfo",1955), ...])

cn.execute("UPDATE libros SET notas = :n WHERE autor = :a",
           {"n": "prestado", "a": "Juan Rulfo"})
salidalastrowid: 1
total ahora: 4

executemany hace el lote dentro de una sola transacción, que en SQLite es la diferencia entre insertar mil filas en un segundo o en dos minutos. Y los parámetros pueden ir por posición con ? o por nombre con :nombre; con más de tres o cuatro, los nombres se leen mucho mejor.

Consultar

for f in cn.execute("SELECT * FROM libros WHERE anio < ? ORDER BY anio", (1990,)):
    print(f["id"], f["titulo"], f["autor"], f["notas"] or "(sin notas)")
salida3  Ficciones                  Jorge Luis Borges  1944  (sin notas)
2  Pedro Páramo               Juan Rulfo         1955  prestado
1  Rayuela                    Julio Cortázar     1963  (sin notas)
4  La casa de los espíritus   Isabel Allende     1982  (sin notas)

El cursor es iterable, así que no hace falta fetchall(): con una tabla grande, iterarlo no carga todo en memoria. Para una sola fila, fetchone(), que devuelve None si no hay nada.

El fallo que comete todo el mundo la primera vez:

cn.execute("... WHERE autor = ?", "Juan Rulfo")ProgrammingError: Incorrect number of bindings supplied. The current statement uses 1, and there are 10 supplied.

Los parámetros van en una secuencia, y una cadena ya es una secuencia: de sus diez caracteres. Con un solo parámetro hay que escribir la coma: ("Juan Rulfo",). Esos diez del mensaje son las letras de «Juan Rulfo».

Las restricciones

salidaIntegrityError: UNIQUE constraint failed: libros.titulo, libros.autor

IntegrityError cubre las violaciones de UNIQUE, NOT NULL, CHECK y las claves ajenas. Capturarlo es la forma de distinguir «ya existe» de un error de verdad, y suele ir con INSERT ... ON CONFLICT DO NOTHING cuando lo que quieres es ignorarlo.

Los interrogantes no son concatenación

malo = "x'; DROP TABLE libros; --"
cn.execute("SELECT COUNT(*) FROM libros WHERE autor = ?", (malo,))
salidabuscando "x'; DROP TABLE libros; --" -> 0 filas
y la tabla sigue con 4 libros

El valor viaja aparte y la base de datos nunca lo interpreta como SQL. Deja de ser cierto en cuanto construyes la consulta con una f-string, y ahí es donde aparecen las inyecciones. Lo que no se puede parametrizar son los nombres de tabla y de columna; si tienen que ser variables, valídalos contra una lista blanca.

El with no cierra la conexión

with sqlite3.connect(p) as cn:
    cn.execute("INSERT ...")
# aquí se ha confirmado la transacción, pero la conexión SIGUE ABIERTA
cn.close()
salidatras close(): Cannot operate on a closed database.
Esto es una trampa de la biblioteca, no tuya. En casi todo Python, salir de un with cierra el recurso. Aquí no: el with de sqlite3 gestiona la transacción —confirma al salir bien, deshace si hubo excepción— y deja la conexión abierta. Hay que llamar a close(), o envolverla en contextlib.closing.
Para… Usa Y no olvides
Conectar sqlite3.connect(ruta) close() al final
Filas con nombre cn.row_factory = sqlite3.Row
Claves ajenas PRAGMA foreign_keys = ON no está activo de serie
Muchas filas executemany va en una transacción
Un solo parámetro (valor,) la coma
Valores del usuario siempre ? o :nombre nunca una f-string
Varios escritores PRAGMA journal_mode = WAL

Una base de datos en memoria, para pruebas: sqlite3.connect(":memory:"). Arranca al instante y desaparece sola.

Todo el código se ejecutó con Python 3.14 en un contenedor limpio antes de publicar esta página; las salidas están copiadas de esa ejecución.